├── .npmignore ├── packages ├── example-app │ ├── nodemon.json │ ├── public │ │ └── index.html │ ├── src │ │ ├── config.ts │ │ └── controller │ │ │ └── main.http.tsx │ ├── tsconfig.json │ ├── package.json │ └── app.ts ├── docs │ ├── 2022-06-05-00-20-12.png │ ├── 2022-06-05-00-21-04.png │ └── logo.svg ├── deepkit-openapi │ ├── index.ts │ ├── src │ │ ├── module.config.ts │ │ ├── service.ts │ │ └── module.ts │ ├── tsconfig.json │ └── package.json ├── deepkit-openapi-core │ ├── jest.config.js │ ├── index.ts │ ├── src │ │ ├── utils.ts │ │ ├── annotations.ts │ │ ├── errors.ts │ │ ├── validators.ts │ │ ├── types.ts │ │ ├── SchemaRegistry.ts │ │ ├── ParametersResolver.ts │ │ ├── TypeSchemaResolver.spec.ts │ │ ├── OpenAPIDocument.ts │ │ └── TypeSchemaResolver.ts │ ├── tsconfig.json │ ├── package.json │ └── scripts │ │ └── index.ts └── example-get-started │ ├── tsconfig.json │ ├── package.json │ └── app.ts ├── .yarnrc.yml ├── tsconfig.json ├── .vscode └── launch.json ├── package.json ├── LICENSE ├── DEVELOPMENT.md ├── .gitignore ├── .circleci └── config.yml └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/example-app/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignoreRoot": [".git"] 3 | } -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-4.6.0.cjs 2 | nodeLinker: node-modules 3 | -------------------------------------------------------------------------------- /packages/docs/2022-06-05-00-20-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanayashiki/deepkit-openapi/HEAD/packages/docs/2022-06-05-00-20-12.png -------------------------------------------------------------------------------- /packages/docs/2022-06-05-00-21-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanayashiki/deepkit-openapi/HEAD/packages/docs/2022-06-05-00-21-04.png -------------------------------------------------------------------------------- /packages/deepkit-openapi/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/service'; 2 | export * from './src/module'; 3 | export * from 'deepkit-openapi-core'; -------------------------------------------------------------------------------- /packages/deepkit-openapi-core/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /packages/deepkit-openapi-core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/OpenAPIDocument'; 2 | export * from './src/types'; 3 | export * from './src/annotations'; 4 | 5 | export type { RegistableSchema } from './src/SchemaRegistry'; 6 | -------------------------------------------------------------------------------- /packages/deepkit-openapi-core/src/utils.ts: -------------------------------------------------------------------------------- 1 | export const resolveOpenApiPath = (deepkitPath: string) => { 2 | let s = deepkitPath.replace(/:(\w+)/g, (_, name) => `\{${name}\}`) 3 | s = !s.startsWith('/') ? '/' + s : s; 4 | return s; 5 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "packages/example-app/tsconfig.json" 6 | }, 7 | { 8 | "path": "packages/deepkit-openapi/tsconfig.json" 9 | }, 10 | { 11 | "path": "packages/deepkit-openapi-core/tsconfig.json" 12 | }, 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/example-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | This is the title of the webpage! 5 | 6 | 7 |

This is an example paragraph. Anything in the body tag will appear on the page, just like this p tag and its contents.

8 | 9 | -------------------------------------------------------------------------------- /packages/example-app/src/config.ts: -------------------------------------------------------------------------------- 1 | export class Config { 2 | dbPath: string = '/tmp/myapp.sqlite'; 3 | 4 | /** 5 | * @description In development we enable FrameworkModule debugger. 6 | * In production we enable JSON logger. 7 | */ 8 | environment: 'development' | 'production' = 'development'; 9 | } 10 | -------------------------------------------------------------------------------- /packages/deepkit-openapi/src/module.config.ts: -------------------------------------------------------------------------------- 1 | import { OpenAPICoreConfig } from "deepkit-openapi-core"; 2 | 3 | export class OpenAPIConfig extends OpenAPICoreConfig { 4 | title: string = "OpenAPI"; 5 | description: string = ""; 6 | version: string = "1.0.0"; 7 | // Prefix for all OpenAPI related controllers 8 | prefix: string = "/openapi/"; 9 | } 10 | -------------------------------------------------------------------------------- /packages/deepkit-openapi-core/src/annotations.ts: -------------------------------------------------------------------------------- 1 | import { TypeAnnotation } from "@deepkit/core"; 2 | 3 | export type Format = TypeAnnotation<'openapi:format', Name>; 4 | export type Default any)> = TypeAnnotation<'openapi:default', Value>; 5 | export type Description = TypeAnnotation<'openapi:description', Text>; 6 | export type Deprecated = TypeAnnotation<'openapi:deprecated', true>; 7 | export type Name = TypeAnnotation<'openapi:name', Text>; 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Attach", 9 | "port": 9229, 10 | "request": "attach", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "type": "node" 15 | } 16 | 17 | ] 18 | } -------------------------------------------------------------------------------- /packages/deepkit-openapi-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "sourceMap": true, 5 | "declaration": true, 6 | "experimentalDecorators": true, 7 | "moduleResolution": "node", 8 | "target": "es2018", 9 | "module": "CommonJS", 10 | "preserveSymlinks": true, 11 | "esModuleInterop": true, 12 | "lib": [ 13 | "es6", 14 | "dom" 15 | ], 16 | "outDir": "./dist/cjs", 17 | "composite": true 18 | }, 19 | "reflection": true, 20 | "includes": [ 21 | "*.ts", 22 | "*.tsx" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /packages/docs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/example-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "sourceMap": true, 5 | "declaration": false, 6 | "experimentalDecorators": true, 7 | "moduleResolution": "node", 8 | "target": "es2018", 9 | "module": "CommonJS", 10 | "preserveSymlinks": true, 11 | "esModuleInterop": true, 12 | "lib": [ 13 | "es6", 14 | "dom" 15 | ], 16 | "outDir": "./dist", 17 | "jsx": "react-jsx", 18 | "jsxImportSource": "@deepkit/template" 19 | }, 20 | "reflection": true, 21 | "includes": [ 22 | "*.ts", 23 | "*.tsx" 24 | ], 25 | "references": [ 26 | { "path": "../deepkit-openapi" } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /packages/example-get-started/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "sourceMap": true, 5 | "declaration": false, 6 | "experimentalDecorators": true, 7 | "moduleResolution": "node", 8 | "target": "es2018", 9 | "module": "CommonJS", 10 | "preserveSymlinks": true, 11 | "esModuleInterop": true, 12 | "lib": [ 13 | "es6", 14 | "dom" 15 | ], 16 | "outDir": "./dist", 17 | "jsx": "react-jsx", 18 | "jsxImportSource": "@deepkit/template" 19 | }, 20 | "reflection": true, 21 | "includes": [ 22 | "*.ts", 23 | "*.tsx" 24 | ], 25 | "references": [ 26 | { "path": "../deepkit-openapi" } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /packages/deepkit-openapi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "sourceMap": true, 5 | "declaration": true, 6 | "experimentalDecorators": true, 7 | "moduleResolution": "node", 8 | "target": "es2018", 9 | "module": "CommonJS", 10 | "preserveSymlinks": true, 11 | "esModuleInterop": true, 12 | "lib": [ 13 | "es6", 14 | "dom" 15 | ], 16 | "outDir": "./dist/cjs", 17 | "jsx": "react-jsx", 18 | "jsxImportSource": "@deepkit/template", 19 | "composite": true 20 | }, 21 | "reflection": true, 22 | "includes": [ 23 | "*.ts", 24 | "*.tsx" 25 | ], 26 | "references": [ 27 | { "path": "../deepkit-openapi-core" } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "packages/*" 5 | ], 6 | "name": "deepkit-openapi-monorepo", 7 | "version": "1.0.0", 8 | "description": "", 9 | "main": "index.js", 10 | "scripts": { 11 | "postinstall": "deepkit-type-install", 12 | "test": "echo \"Error: no test specified\" && exit 1", 13 | "tsc": "tsc --build", 14 | "tsc-watch": "tsc --build --watch" 15 | }, 16 | "author": "hanayashiki", 17 | "license": "ISC", 18 | "dependencies": { 19 | "@deepkit/type-compiler": "^1.0.2", 20 | "npm-local-development": "^0.4.2", 21 | "ts-node": "^10.9.2", 22 | "ts-node-dev": "^2.0.0", 23 | "typescript": "~5.7" 24 | }, 25 | "packageManager": "yarn@4.6.0" 26 | } 27 | -------------------------------------------------------------------------------- /packages/deepkit-openapi/src/service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpRouteFilter, 3 | HttpRouterFilterResolver 4 | } from "@deepkit/http"; 5 | import { ScopedLogger } from "@deepkit/logger"; 6 | import { OpenAPIDocument } from "deepkit-openapi-core"; 7 | import { OpenAPIConfig } from "./module.config"; 8 | 9 | 10 | export class OpenAPIService { 11 | constructor( 12 | private routerFilter: HttpRouteFilter, 13 | protected filterResolver: HttpRouterFilterResolver, 14 | private logger: ScopedLogger, 15 | private config: OpenAPIConfig 16 | ) {} 17 | 18 | serialize() { 19 | const routes = this.filterResolver.resolve(this.routerFilter.model); 20 | const openApiDocument = new OpenAPIDocument(routes, this.logger, this.config); 21 | const result = openApiDocument.serializeDocument(); 22 | 23 | return result; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/example-get-started/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-get-started", 3 | "private": true, 4 | "version": "0.0.1", 5 | "description": "", 6 | "scripts": { 7 | "start": "ts-node app.ts server:start", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "dependencies": { 11 | "@deepkit/app": "^1.0.2", 12 | "@deepkit/broker": "^1.0.2", 13 | "@deepkit/bson": "^1.0.2", 14 | "@deepkit/core-rxjs": "^1.0.1", 15 | "@deepkit/framework": "^1.0.2", 16 | "@deepkit/http": "^1.0.2", 17 | "@deepkit/logger": "^1.0.2", 18 | "@deepkit/orm": "^1.0.2", 19 | "@deepkit/rpc": "^1.0.2", 20 | "@deepkit/rpc-tcp": "^1.0.2", 21 | "@deepkit/sql": "^1.0.2", 22 | "@deepkit/sqlite": "^1.0.2", 23 | "@deepkit/type": "^1.0.2", 24 | "deepkit-openapi": "workspace:*", 25 | "rxjs": "^7.8.2", 26 | "ts-node": "^10.9.2", 27 | "yaml": "^2.7.0" 28 | }, 29 | "keywords": [], 30 | "author": "", 31 | "license": "MIT" 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Hanayashiki 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /packages/deepkit-openapi-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deepkit-openapi-core", 3 | "version": "0.0.9", 4 | "description": "", 5 | "main": "./dist/cjs/index.js", 6 | "types": "./dist/cjs/index.d.ts", 7 | "exports": { 8 | ".": { 9 | "require": "./dist/cjs/index.js" 10 | } 11 | }, 12 | "scripts": { 13 | "play": "node --watch dist/cjs/scripts/index.js", 14 | "test": "jest" 15 | }, 16 | "devDependencies": { 17 | "@deepkit/core": "^1.0.1", 18 | "@deepkit/http": "^1.0.2", 19 | "@deepkit/logger": "^1.0.2", 20 | "@deepkit/type": "^1.0.2", 21 | "@types/jest": "^29.5.14", 22 | "@types/lodash.clonedeepwith": "4.5.9", 23 | "jest": "^29.7.0", 24 | "ts-jest": "^29.2.6", 25 | "yaml": "2.7.0" 26 | }, 27 | "peerDependencies": { 28 | "@deepkit/core": "^1.0.1", 29 | "@deepkit/http": "^1.0.2", 30 | "@deepkit/type": "^1.0.2" 31 | }, 32 | "dependencies": { 33 | "camelcase": "6.3.0", 34 | "lodash.clonedeepwith": "4.5.0" 35 | }, 36 | "keywords": [], 37 | "author": "", 38 | "license": "MIT", 39 | "publishConfig": { 40 | "access": "public" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/example-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@deepkit/example-app", 3 | "private": true, 4 | "version": "1.0.1-alpha.0", 5 | "description": "", 6 | "scripts": { 7 | "start": "node --enable-source-maps --inspect --watch dist/app server:start", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "dependencies": { 11 | "@deepkit/app": "^1.0.1-alpha.131", 12 | "@deepkit/broker": "^1.0.1-alpha.131", 13 | "@deepkit/bson": "^1.0.1-alpha.131", 14 | "@deepkit/core-rxjs": "^1.0.1-alpha.124", 15 | "@deepkit/filesystem": "^1.0.1-alpha.124", 16 | "@deepkit/framework": "^1.0.1-alpha.131", 17 | "@deepkit/http": "^1.0.1-alpha.131", 18 | "@deepkit/logger": "^1.0.1-alpha.131", 19 | "@deepkit/orm": "^1.0.1-alpha.131", 20 | "@deepkit/rpc": "^1.0.1-alpha.131", 21 | "@deepkit/rpc-tcp": "^1.0.1-alpha.131", 22 | "@deepkit/sql": "^1.0.1-alpha.131", 23 | "@deepkit/sqlite": "^1.0.1-alpha.131", 24 | "@deepkit/type": "^1.0.1-alpha.131", 25 | "deepkit-openapi": "workspace:*", 26 | "rxjs": "^7.8.1", 27 | "yaml": "^2.3.4" 28 | }, 29 | "keywords": [], 30 | "author": "", 31 | "license": "MIT" 32 | } 33 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Dev Manual 2 | 3 | ## Development 4 | 5 | To initialize the project: 6 | 7 | ```bash 8 | git clone https://github.com/hanayashiki/deepkit-openapi 9 | cd deepkit-openapi 10 | yarn 11 | ``` 12 | 13 | To build the libraries, run the following command at the monorepo root: 14 | 15 | ``` 16 | yarn tsc-watch 17 | ``` 18 | 19 | ## Backlogs 20 | 21 | ### Functional 22 | 23 | 1. Simple type casting problems 24 | 1. string, Date 25 | 2. float, string ? 26 | 2. Handle serializer options 27 | 1. Renaming 28 | 2. Exclusion 29 | 3. Groups 30 | 3. ~~Swagger UI Hosting~~ 31 | 32 | ### Operational 33 | 34 | 1. ~~Unit tests~~ 35 | 2. CI 36 | 37 | ## Limitations 38 | 39 | 1. Functional routers not supported. 40 | 41 | ```ts 42 | // Will not be documented 43 | router.get('/user/:id', async (id: number, database: Database) => { 44 | return await database.query(User).filter({id}).findOne(); 45 | }); 46 | ``` 47 | 48 | 2. Parameter default values cannot depend on other parameters. 49 | 50 | 3. Parameter resolver 51 | 52 | ```ts 53 | @http.resolveParameter(User, UserResolver) 54 | class MyWebsite { 55 | // Does not know user is derived from path parameter `id: number` 56 | @http.GET(':id') 57 | getUser(user: User) { 58 | return 'Hello ' + user.username; 59 | } 60 | } 61 | ``` 62 | 63 | 4. Binary fields: `Uint8Array` etc. are not documented. 64 | 65 | 5. Content type other than `application/json` are not documentated 66 | 67 | ## References 68 | 69 | [Deepkit Framework](https://deepkit.io/documentation/framework) 70 | 71 | [Deepkit Book](https://deepkit-book.herokuapp.com/deepkit-book-english.html#_input) 72 | 73 | -------------------------------------------------------------------------------- /packages/deepkit-openapi-core/scripts/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | typeOf, 3 | MinLength, 4 | TypedArray, 5 | reflect, 6 | TypeEnum, 7 | metaAnnotation, 8 | stringifyType, 9 | } from "@deepkit/type"; 10 | import { unwrapTypeSchema } from "../src/TypeSchemaResolver"; 11 | import { stringify } from "yaml"; 12 | import { SchemaRegistry } from "../src/SchemaRegistry"; 13 | import { HttpQueries } from "@deepkit/http"; 14 | 15 | // console.log(typeOf<'a' | 'b' | 'c'>()); 16 | 17 | // console.log(typeOf<'a' | 'b' | 'c'>()); 18 | 19 | // console.log(typeOf<'a' | 'b' | string>()); 20 | 21 | type U = "a" | "b" | "c"; 22 | 23 | type W = "a" | "b" | "c" | T; 24 | 25 | // console.log(typeOf()); 26 | 27 | // console.log(typeOf>()); 28 | 29 | enum E { 30 | a = 1, 31 | b = "b", 32 | } 33 | 34 | enum E2 { 35 | a = "a", 36 | b = "b", 37 | } 38 | 39 | // console.log((typeOf() as TypeEnum).indexType); 40 | 41 | // console.log((typeOf() as TypeEnum).indexType); 42 | 43 | // console.log(unwrapTypeSchema(typeOf())); 44 | 45 | // console.log(unwrapTypeSchema(typeOf())); 46 | 47 | // console.log(unwrapTypeSchema(typeOf())); 48 | 49 | // console.log(unwrapTypeSchema(typeOf<"a" | "b" | "c">())); 50 | 51 | console.log(unwrapTypeSchema(typeOf>())); 52 | 53 | type ActionOne = { 54 | type: "one"; 55 | arg1: number; 56 | }; 57 | 58 | type ActionTwo = { 59 | type: "two"; 60 | arg2: string; 61 | }; 62 | 63 | type Action = ActionOne | ActionTwo; 64 | 65 | console.log(typeOf()); 66 | 67 | console.log(unwrapTypeSchema(typeOf())); 68 | 69 | type CustomA = { __meta?: ["CustomA"] }; 70 | type CustomB = { __meta?: ["CustomB"] }; 71 | 72 | type O = {} & CustomA; 73 | type Decorate = T & CustomB; 74 | 75 | const decorated = typeOf>(); 76 | 77 | console.log(metaAnnotation.getAnnotations(decorated)); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | .expo-shared/* 4 | npm-debug.* 5 | *.orig.* 6 | web-build/* 7 | web-report/ 8 | 9 | # macOS 10 | .DS_Store 11 | 12 | # The following contents were automatically generated by expo-cli during eject 13 | # ---------------------------------------------------------------------------- 14 | 15 | # OSX 16 | # 17 | .DS_Store 18 | 19 | # Xcode 20 | # 21 | build/ 22 | *.pbxuser 23 | !default.pbxuser 24 | *.mode1v3 25 | !default.mode1v3 26 | *.mode2v3 27 | !default.mode2v3 28 | *.perspectivev3 29 | !default.perspectivev3 30 | xcuserdata 31 | *.xccheckout 32 | *.moved-aside 33 | DerivedData 34 | *.hmap 35 | *.ipa 36 | *.xcuserstate 37 | project.xcworkspace 38 | 39 | # Android/IntelliJ 40 | # 41 | build/ 42 | .idea 43 | .gradle 44 | local.properties 45 | *.iml 46 | *.hprof 47 | 48 | # node.js 49 | # 50 | node_modules/ 51 | npm-debug.log 52 | yarn-error.log 53 | 54 | # BUCK 55 | buck-out/ 56 | \.buckd/ 57 | *.keystore 58 | !debug.keystore 59 | 60 | # fastlane 61 | # 62 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 63 | # screenshots whenever they are needed. 64 | # For more information about the recommended setup visit: 65 | # https://docs.fastlane.tools/best-practices/source-control/ 66 | 67 | */fastlane/report.xml 68 | */fastlane/Preview.html 69 | */fastlane/screenshots 70 | 71 | # Bundle artifacts 72 | *.jsbundle 73 | 74 | # CocoaPods 75 | /ios/Pods/ 76 | 77 | # Expo 78 | .expo/* 79 | web-build/ 80 | 81 | dist/ 82 | storybook-static/ 83 | 84 | .plugin-lazy-pages/ 85 | .plugin-split-pages/ 86 | .split-pages/ 87 | 88 | tsconfig.tsbuildinfo 89 | 90 | var/ 91 | package-lock.json 92 | 93 | # Yarn 94 | .pnp.* 95 | .yarn/* 96 | !.yarn/patches 97 | !.yarn/plugins 98 | !.yarn/releases 99 | !.yarn/sdks 100 | !.yarn/versions 101 | -------------------------------------------------------------------------------- /packages/deepkit-openapi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deepkit-openapi", 3 | "version": "0.0.9", 4 | "description": "", 5 | "main": "./dist/cjs/index.js", 6 | "types": "./dist/cjs/index.d.ts", 7 | "exports": { 8 | ".": { 9 | "default": "./dist/cjs/index.js" 10 | } 11 | }, 12 | "dependencies": { 13 | "deepkit-openapi-core": "workspace:*", 14 | "swagger-ui-dist": "^5.20.0", 15 | "yaml": "^2.7.0" 16 | }, 17 | "devDependencies": { 18 | "@deepkit/app": "^1.0.2", 19 | "@deepkit/core": "1.0.1", 20 | "@deepkit/event": "^1.0.2", 21 | "@deepkit/framework": "^1.0.2", 22 | "@deepkit/http": "^1.0.2", 23 | "@deepkit/injector": "^1.0.2", 24 | "@deepkit/logger": "^1.0.2", 25 | "@deepkit/stopwatch": "^1.0.2", 26 | "@deepkit/template": "^1.0.2", 27 | "@deepkit/type": "^1.0.2", 28 | "@deepkit/workflow": "^1.0.2", 29 | "@types/send": "^0.17.4", 30 | "@types/swagger-ui-dist": "^3.30.5" 31 | }, 32 | "peerDependencies": { 33 | "@deepkit/app": "^1.0.2", 34 | "@deepkit/core": "1.0.1", 35 | "@deepkit/framework": "^1.0.2", 36 | "@deepkit/http": "^1.0.2", 37 | "@deepkit/injector": "^1.0.2", 38 | "@deepkit/logger": "^1.0.2", 39 | "@deepkit/stopwatch": "^1.0.2", 40 | "@deepkit/template": "^1.0.2", 41 | "@deepkit/type": "^1.0.2", 42 | "@deepkit/workflow": "^1.0.2", 43 | "send": "^0.18.0" 44 | }, 45 | "keywords": [ 46 | "typescript", 47 | "deepkit", 48 | "openapi", 49 | "swagger", 50 | "swagger-ui", 51 | "javascript", 52 | "openapi3", 53 | "reflection", 54 | "http", 55 | "types", 56 | "server", 57 | "nodejs", 58 | "node", 59 | "generator", 60 | "api" 61 | ], 62 | "repository": "https://github.com/hanayashiki/deepkit-openapi", 63 | "author": "Chenyu Wang ", 64 | "license": "MIT", 65 | "publishConfig": { 66 | "access": "public" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/deepkit-openapi-core/src/errors.ts: -------------------------------------------------------------------------------- 1 | import { ClassType, getClassName } from "@deepkit/core"; 2 | import { stringifyType, Type } from "@deepkit/type"; 3 | 4 | export class DeepKitOpenApiError extends Error {} 5 | 6 | export class DeepKitTypeError extends DeepKitOpenApiError {} 7 | 8 | export class TypeNotSupported extends DeepKitTypeError { 9 | constructor(public type: Type, public reason: string = "") { 10 | super(`${stringifyType(type)} is not supported. ${reason}`); 11 | } 12 | } 13 | 14 | export class LiteralSupported extends DeepKitTypeError { 15 | constructor(public typeName: string) { 16 | super(`${typeName} is not supported. `); 17 | } 18 | } 19 | 20 | export class DeepKitTypeErrors extends DeepKitOpenApiError { 21 | constructor(public errors: DeepKitTypeError[], message: string) { 22 | super(message); 23 | } 24 | } 25 | 26 | export class DeepKitOpenApiSchemaNameConflict extends DeepKitOpenApiError { 27 | constructor(public newType: Type, public oldType: Type, public name: string) { 28 | super( 29 | `${stringifyType(newType)} and ${stringifyType( 30 | oldType, 31 | )} are not the same, but their schema are both named as ${JSON.stringify( 32 | name, 33 | )}. ` + 34 | `Try to fix the naming of related types, or rename them using 'YourClass & Name'`, 35 | ); 36 | } 37 | } 38 | 39 | export class DeepKitOpenApiControllerNameConflict extends DeepKitOpenApiError { 40 | constructor( 41 | public newController: ClassType, 42 | public oldController: ClassType, 43 | public name: string, 44 | ) { 45 | super( 46 | `${getClassName(newController)} and ${getClassName( 47 | oldController, 48 | )} are both tagged as ${name}. ` + `Please consider renaming them. `, 49 | ); 50 | } 51 | } 52 | 53 | export class DeepKitOpenApiOperationNameConflict extends DeepKitOpenApiError { 54 | constructor( 55 | public fullPath: string, 56 | public method: string, 57 | ) { 58 | super(`Operation ${method} ${fullPath} is repeated. Please consider renaming them. `); 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /packages/example-app/src/controller/main.http.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | http, 3 | HttpBody, 4 | HttpBodyValidation, 5 | HttpQueries, 6 | HttpQuery, 7 | UploadedFile, 8 | } from "@deepkit/http"; 9 | import { LoggerInterface } from "@deepkit/logger"; 10 | import { OpenAPIService } from "deepkit-openapi"; 11 | 12 | enum Country { 13 | china = "cn", 14 | japan = "jp", 15 | india = "in", 16 | } 17 | 18 | type UserStatus = "active" | "inactive"; 19 | 20 | class User { 21 | country: Country = Country.india; 22 | status: UserStatus = "active"; 23 | name: string = ""; 24 | } 25 | 26 | class AddUserDto extends User {} 27 | 28 | class UserNameOnly { 29 | username: string = ""; 30 | } 31 | 32 | class UploadedFiles { 33 | files!: UploadedFile | UploadedFile[]; 34 | } 35 | 36 | class ArrayInputItem { 37 | name: string = ""; 38 | age: number = 0; 39 | } 40 | 41 | @http.controller() 42 | export class MainController { 43 | constructor( 44 | protected logger: LoggerInterface, 45 | protected openApi: OpenAPIService 46 | ) {} 47 | 48 | @(http.POST("/add").description("Adds a new user")) 49 | async add(body: HttpBodyValidation) { 50 | return body; 51 | } 52 | 53 | @(http.POST("/upload").description("Uploaded files")) 54 | async upload(body: HttpBody) { 55 | return body; 56 | } 57 | 58 | @http.POST("/api/array-input") 59 | async arrayInput(body: HttpBody) { 60 | return body; 61 | } 62 | 63 | @http.GET("/query") 64 | async queryParam( 65 | shit: HttpQuery, 66 | peter: HttpQuery = "peter" 67 | ) { 68 | return peter; 69 | } 70 | 71 | @http.GET("/omit") 72 | async omit(shit?: HttpQuery) { 73 | return shit; 74 | } 75 | 76 | @http.GET("/queryWithQueries") 77 | async queryWithQueries( 78 | shit: HttpQuery, 79 | queries: HttpQueries<{ limit: number; offset: number }> 80 | ) { 81 | return shit; 82 | } 83 | 84 | @http.GET("/twoQueries") 85 | async twoQueries( 86 | q1: HttpQueries<{ a: number; b: string }>, 87 | q2: HttpQueries<{ a: string }> 88 | ) { 89 | return typeof q1.a; 90 | } 91 | 92 | @(http.GET("/withResponse").response(200, "Only name is showed", UserNameOnly)) 93 | async withResponse() { 94 | return new User(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Use the latest 2.1 version of CircleCI pipeline process engine. 2 | # See: https://circleci.com/docs/2.0/configuration-reference 3 | version: 2.1 4 | 5 | orbs: 6 | # The Node.js orb contains a set of prepackaged CircleCI configuration you can utilize 7 | # Orbs reduce the amount of configuration required for common tasks. 8 | # See the orb documentation here: https://circleci.com/developer/orbs/orb/circleci/node 9 | node: circleci/node@4.1 10 | browser-tools: circleci/browser-tools@1.2.5 11 | aws-s3: circleci/aws-s3@3.0.0 12 | 13 | jobs: 14 | # Below is the definition of your job to build and test your app, you can rename and customize it as you want. 15 | build-and-publish: 16 | # These next lines define a Docker executor: https://circleci.com/docs/2.0/executor-types/ 17 | # You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. 18 | # A list of available CircleCI Docker Convenience Images are available here: https://circleci.com/developer/images/image/cimg/node 19 | docker: 20 | - image: cimg/node:22.15 21 | # Then run your tests! 22 | # CircleCI will report the results back to your VCS provider. 23 | steps: 24 | # Checkout the code as the first step. 25 | - checkout 26 | - node/install-packages: 27 | pkg-manager: yarn 28 | - restore_cache: 29 | name: Restore Yarn Package Cache 30 | keys: 31 | - yarn-packages-{{ checksum "yarn.lock" }} 32 | - save_cache: 33 | name: Save Yarn Package Cache 34 | key: yarn-packages-{{ checksum "yarn.lock" }} 35 | paths: 36 | - ~/.cache/yarn 37 | - run: 38 | name: Authenticate with registry 39 | command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc 40 | - run: 41 | name: Build and Publish 42 | command: | 43 | yarn postinstall 44 | yarn tsc 45 | 46 | cp README.md /home/circleci/project/packages/deepkit-openapi/ 47 | cp README.md /home/circleci/project/packages/deepkit-openapi-core/ 48 | 49 | cd /home/circleci/project/packages/deepkit-openapi-core 50 | npm publish || true 51 | cd /home/circleci/project/packages/deepkit-openapi 52 | npm publish || true 53 | 54 | workflows: 55 | # Below is the definition of your workflow. 56 | # Inside the workflow, you provide the jobs you want to run, e.g this workflow runs the build-and-test job above. 57 | # CircleCI will run this workflow on every commit. 58 | # For more details on extending your workflow, see the configuration docs: https://circleci.com/docs/2.0/configuration-reference/#workflows 59 | deploy: 60 | jobs: 61 | - build-and-publish: 62 | filters: 63 | branches: 64 | only: 65 | - master 66 | -------------------------------------------------------------------------------- /packages/example-app/app.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ts-node-script 2 | import { http, HttpBody, HttpError } from "@deepkit/http"; 3 | 4 | import { FrameworkModule } from "@deepkit/framework"; 5 | import { JSONTransport, Logger } from "@deepkit/logger"; 6 | import { App } from "@deepkit/app"; 7 | import { OpenAPIModule, Name } from "deepkit-openapi"; 8 | import { Email, MaxLength, metaAnnotation, typeOf, validationAnnotation } from "@deepkit/type"; 9 | import { MainController } from "./src/controller/main.http"; 10 | import { Config } from "./src/config"; 11 | 12 | type User = { 13 | id: number; 14 | name: string; 15 | email: string & Email; 16 | password: string; 17 | }; 18 | 19 | type CreateUser = Omit; 20 | 21 | type UpdateUser = Partial & Name<'UpdateUser'>; 22 | 23 | type ReadUser = Omit & Name<'ReadUser'>;; 24 | 25 | const db: User[] = [ 26 | { id: 1, name: "Bob", email: "bob@gmail.com", password: "123" }, 27 | { id: 2, name: "Alice", email: "alice@gmail.com", password: "123" }, 28 | ]; 29 | 30 | @http.controller() 31 | class UserController { 32 | @(http.GET("/user/:id").response(200, "Read a User by its ID")) 33 | read(id: number) { 34 | return db.find((user) => user.id === id); 35 | } 36 | 37 | @(http.POST("/user").response(200, "Create a User")) 38 | create(user: HttpBody) { 39 | const newUser = { ...user, id: Date.now() }; 40 | db.push(newUser); 41 | return user; 42 | } 43 | 44 | @(http.PATCH("/user/:id").response(200, "Patch a User's attributes")) 45 | patch(id: number, patch: HttpBody) { 46 | const user = db.find((user) => user.id === id); 47 | 48 | if (user) { 49 | Object.assign(user, patch); 50 | } 51 | 52 | return user; 53 | } 54 | 55 | @(http.DELETE("/user/:id").response(200, "Delete a User by its ID")) 56 | delete(id: number) { 57 | const index = db.findIndex((user) => user.id === id); 58 | 59 | if (index !== -1) { 60 | const user = db[index]; 61 | db.splice(index, 1); 62 | return user; 63 | } 64 | 65 | throw new HttpError('User not found', 404); 66 | } 67 | } 68 | 69 | new App({ 70 | config: Config, 71 | providers: [UserController, MainController], 72 | controllers: [UserController, MainController], 73 | imports: [ 74 | new OpenAPIModule({ prefix: "/openapi/" }), 75 | new FrameworkModule({ 76 | publicDir: "public", 77 | httpLog: true, 78 | migrateOnStartup: true, 79 | }), 80 | ], 81 | }) 82 | .setup((module, config) => { 83 | if (config.environment === "development") { 84 | module 85 | .getImportedModuleByClass(FrameworkModule) 86 | .configure({ debug: true }); 87 | } 88 | 89 | if (config.environment === "production") { 90 | //enable logging JSON messages instead of formatted strings 91 | module.configureProvider(p => p.setTransport([new JSONTransport()])); 92 | } 93 | }) 94 | .loadConfigFromEnv() 95 | .run(); 96 | -------------------------------------------------------------------------------- /packages/example-get-started/app.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpBody, HttpError } from "@deepkit/http"; 2 | 3 | import { FrameworkModule } from "@deepkit/framework"; 4 | import { JSONTransport, Logger } from "@deepkit/logger"; 5 | import { App } from "@deepkit/app"; 6 | import { OpenAPIModule } from "deepkit-openapi"; 7 | import { Email, typeOf } from "@deepkit/type"; 8 | 9 | interface User { 10 | id: number; 11 | name: string; 12 | email: string & Email; 13 | password: string; 14 | } 15 | 16 | type CreateUser = Omit; 17 | 18 | type UpdateUser = Partial; 19 | 20 | type ReadUser = Omit; 21 | 22 | const db: User[] = [ 23 | { id: 1, name: "Bob", email: "bob@gmail.com", password: "123" }, 24 | { id: 2, name: "Alice", email: "alice@gmail.com", password: "123" }, 25 | ]; 26 | 27 | @http.controller() 28 | class UserController { 29 | @(http.GET("/user/:id").response(200, "Read a User by its ID")) 30 | read(id: number) { 31 | return db.find((user) => user.id === id); 32 | } 33 | 34 | @(http.POST("/user/").response(200, "Create a User")) 35 | create(user: HttpBody) { 36 | const newUser = { ...user, id: Date.now() }; 37 | db.push(newUser); 38 | return user; 39 | } 40 | 41 | @(http.PATCH("/user/:id").response(200, "Patch a User's attributes")) 42 | patch(id: number, patch: HttpBody) { 43 | const user = db.find((user) => user.id === id); 44 | 45 | if (user) { 46 | Object.assign(user, patch); 47 | } 48 | 49 | return user; 50 | } 51 | 52 | @(http.DELETE("/user/:id").response(200, "Delete a User by its ID")) 53 | delete(id: number) { 54 | const index = db.findIndex((user) => user.id === id); 55 | 56 | if (index !== -1) { 57 | const user = db[index]; 58 | db.splice(index, 1); 59 | return user; 60 | } 61 | 62 | throw new HttpError("User not found", 404); 63 | } 64 | 65 | @(http.GET("/filtered").group("filtered")) 66 | filtered() {} 67 | } 68 | 69 | class Config { 70 | environment: "development" | "production" = "development"; 71 | } 72 | 73 | new App({ 74 | config: Config, 75 | providers: [UserController], 76 | controllers: [UserController], 77 | imports: [ 78 | new OpenAPIModule({ 79 | prefix: "/openapi/", 80 | title: "Users", 81 | description: "Simple user server", 82 | version: "0.0.0", 83 | }).configureHttpRouteFilter((f) => f.excludeRoutes({ group: "filtered" })), 84 | new FrameworkModule({ 85 | publicDir: "public", 86 | httpLog: true, 87 | migrateOnStartup: true, 88 | }), 89 | ], 90 | }) 91 | .setup((module, config) => { 92 | if (config.environment === "development") { 93 | module 94 | .getImportedModuleByClass(FrameworkModule) 95 | .configure({ debug: true }); 96 | } 97 | 98 | if (config.environment === "production") { 99 | //enable logging JSON messages instead of formatted strings 100 | module.configureProvider(p => p.setTransport([new JSONTransport()])); 101 | } 102 | }) 103 | .loadConfigFromEnv() 104 | .run(); 105 | -------------------------------------------------------------------------------- /packages/deepkit-openapi-core/src/validators.ts: -------------------------------------------------------------------------------- 1 | import { TypeLiteral } from "@deepkit/type"; 2 | import { TypeNotSupported } from "./errors"; 3 | import { Schema, SchemaMapper } from "./types"; 4 | 5 | export const validators: Record = { 6 | pattern(s, type: TypeLiteral & { literal: RegExp }): Schema { 7 | return { 8 | ...s, 9 | pattern: type.literal.source, 10 | }; 11 | }, 12 | alpha(s): Schema { 13 | return { 14 | ...s, 15 | pattern: "^[A-Za-z]+$", 16 | }; 17 | }, 18 | alphanumeric(s): Schema { 19 | return { 20 | ...s, 21 | pattern: "^[0-9A-Za-z]+$", 22 | }; 23 | }, 24 | ascii(s): Schema { 25 | return { 26 | ...s, 27 | pattern: "^[\x00-\x7F]+$", 28 | }; 29 | }, 30 | dataURI(s): Schema { 31 | return { 32 | ...s, 33 | pattern: "^(data:)([w/+-]*)(;charset=[w-]+|;base64){0,1},(.*)", 34 | }; 35 | }, 36 | decimal( 37 | s, 38 | minDigits: TypeLiteral & { literal: number }, 39 | maxDigits: TypeLiteral & { literal: number }, 40 | ): Schema { 41 | return { 42 | ...s, 43 | pattern: 44 | "^-?\\d+\\.\\d{" + minDigits.literal + "," + maxDigits.literal + "}$", 45 | }; 46 | }, 47 | multipleOf( 48 | s, 49 | num: TypeLiteral & { literal: number } 50 | ): Schema { 51 | if (num.literal === 0) throw new TypeNotSupported(num, `multiple cannot be 0`); 52 | 53 | return { 54 | ...s, 55 | multipleOf: num.literal, 56 | }; 57 | }, 58 | minLength( 59 | s, 60 | length: TypeLiteral & { literal: number } 61 | ): Schema { 62 | if (length.literal < 0) throw new TypeNotSupported(length, `length cannot be less than 0`); 63 | 64 | return { 65 | ...s, 66 | minLength: length.literal, 67 | }; 68 | }, 69 | maxLength( 70 | s, 71 | length: TypeLiteral & { literal: number } 72 | ): Schema { 73 | if (length.literal < 0) throw new TypeNotSupported(length, `length cannot be less than 0`); 74 | 75 | return { 76 | ...s, 77 | maxLength: length.literal, 78 | }; 79 | }, 80 | includes(s, include: TypeLiteral): Schema { 81 | throw new TypeNotSupported(include, `includes is not supported. `); 82 | }, 83 | excludes(s, exclude: TypeLiteral): Schema { 84 | throw new TypeNotSupported(exclude, `excludes is not supported. `); 85 | }, 86 | minimum(s, min: TypeLiteral & { literal: number | bigint }): Schema { 87 | return { 88 | ...s, 89 | minimum: min.literal, 90 | } 91 | }, 92 | exclusiveMinimum(s, min: TypeLiteral & { literal: number | bigint }): Schema { 93 | return { 94 | ...s, 95 | exclusiveMinimum: min.literal, 96 | }; 97 | }, 98 | maximum(s, max: TypeLiteral & { literal: number | bigint }): Schema { 99 | return { 100 | ...s, 101 | maximum: max.literal, 102 | }; 103 | }, 104 | exclusiveMaximum(s, max: TypeLiteral & { literal: number | bigint }): Schema { 105 | return { 106 | ...s, 107 | exclusiveMaximum: max.literal, 108 | }; 109 | }, 110 | positive(s): Schema { 111 | return { 112 | ...s, 113 | exclusiveMinimum: 0, 114 | } 115 | }, 116 | negative(s): Schema { 117 | return { 118 | ...s, 119 | exclusiveMaximum: 0, 120 | } 121 | }, 122 | }; 123 | 124 | -------------------------------------------------------------------------------- /packages/deepkit-openapi-core/src/types.ts: -------------------------------------------------------------------------------- 1 | import { ClassType } from "@deepkit/core"; 2 | import type { parseRouteControllerAction } from "@deepkit/http"; 3 | 4 | export type SchemaMapper = (s: Schema, ...args: any[]) => Schema; 5 | 6 | export type SimpleType = string | number | boolean | null | bigint; 7 | 8 | export type Schema = { 9 | __type: "schema"; 10 | __registryKey?: string; 11 | __isComponent?: boolean; 12 | __isUndefined?: boolean; 13 | type?: string; 14 | not?: Schema; 15 | pattern?: string; 16 | multipleOf?: number; 17 | minLength?: number; 18 | maxLength?: number; 19 | minimum?: number | bigint; 20 | exclusiveMinimum?: number | bigint; 21 | maximum?: number | bigint; 22 | exclusiveMaximum?: number | bigint; 23 | enum?: SimpleType[]; 24 | properties?: Record; 25 | required?: string[]; 26 | nullable?: boolean; 27 | items?: Schema; 28 | default?: any; 29 | oneOf?: Schema[]; 30 | 31 | $ref?: string; 32 | }; 33 | 34 | export const AnySchema: Schema = { __type: "schema" }; 35 | 36 | export const NumberSchema: Schema = { __type: "schema", type: "number" }; 37 | 38 | export const StringSchema: Schema = { __type: "schema", type: "string" }; 39 | 40 | export const BooleanSchema: Schema = { __type: "schema", type: "boolean" }; 41 | 42 | export type RequestMediaTypeName = 43 | | "application/x-www-form-urlencoded" 44 | | "multipart/form-data" 45 | | "application/json"; 46 | 47 | export type Tag = { 48 | __controller: ClassType; 49 | name: string; 50 | }; 51 | 52 | export type OpenAPIResponse = { 53 | description: string, 54 | content: { 55 | "application/json"?: MediaType; 56 | }, 57 | }; 58 | 59 | export type Responses = Record; 60 | 61 | export type Operation = { 62 | __path: string; 63 | __method: string; 64 | tags: string[]; 65 | summary?: string; 66 | description?: string; 67 | operationId?: string; 68 | deprecated?: boolean; 69 | parameters?: Parameter[]; 70 | requestBody?: RequestBody; 71 | responses?: Responses; 72 | }; 73 | 74 | export type RequestBody = { 75 | content: Record; 76 | required?: boolean; 77 | }; 78 | 79 | export type MediaType = { 80 | schema?: Schema; 81 | example?: any; 82 | }; 83 | 84 | export type Path = { 85 | summary?: string; 86 | description?: string; 87 | get?: Operation; 88 | put?: Operation; 89 | post?: Operation; 90 | delete?: Operation; 91 | options?: Operation; 92 | head?: Operation; 93 | patch?: Operation; 94 | trace?: Operation; 95 | }; 96 | 97 | export type HttpMethod = 98 | | "get" 99 | | "put" 100 | | "post" 101 | | "delete" 102 | | "options" 103 | | "head" 104 | | "patch" 105 | | "trace"; 106 | 107 | export type ParameterIn = "query" | "header" | "path" | "cookie"; 108 | 109 | export type Parameter = { 110 | in: ParameterIn; 111 | name: string; 112 | required?: boolean; 113 | deprecated?: boolean; 114 | schema?: Schema; 115 | }; 116 | 117 | export type ParsedRoute = ReturnType; 118 | 119 | export type ParsedRouteParameter = ParsedRoute["parameters"][number]; 120 | 121 | export type Info = { 122 | title: string; 123 | description?: string; 124 | termsOfService?: string; 125 | contact: {}; 126 | license: {}; 127 | version: string; 128 | }; 129 | 130 | export type Components = { 131 | schemas?: Record; 132 | }; 133 | 134 | export type OpenAPI = { 135 | openapi: string; 136 | info: Info; 137 | servers: {}[]; 138 | paths: Record; 139 | components: Components; 140 | }; 141 | -------------------------------------------------------------------------------- /packages/deepkit-openapi-core/src/SchemaRegistry.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isPrimitive, 3 | isSameType, 4 | metaAnnotation, 5 | ReflectionKind, 6 | stringifyType, 7 | Type, 8 | TypeClass, 9 | TypeEnum, 10 | TypeLiteral, 11 | TypeObjectLiteral, 12 | typeOf, 13 | TypeUnion, 14 | } from "@deepkit/type"; 15 | import camelcase from "camelcase"; 16 | import { DeepKitOpenApiSchemaNameConflict } from "./errors"; 17 | import { Schema } from "./types"; 18 | 19 | export interface SchemeEntry { 20 | name: string; 21 | schema: Schema; 22 | type: Type; 23 | } 24 | 25 | export type RegistableSchema = 26 | | TypeClass 27 | | TypeObjectLiteral 28 | | TypeEnum 29 | | TypeUnion; 30 | 31 | export type SchemaKeyFn = (t: RegistableSchema) => string | undefined; 32 | 33 | export class SchemaRegistry { 34 | store: Map = new Map(); 35 | 36 | constructor(private customSchemaKeyFn?: SchemaKeyFn) {} 37 | 38 | getSchemaKey(t: RegistableSchema): string { 39 | const nameAnnotation = metaAnnotation 40 | .getAnnotations(t) 41 | .find((t) => t.name === "openapi:name"); 42 | 43 | // Handle user preferred name 44 | if (nameAnnotation?.options.kind === ReflectionKind.literal) { 45 | return nameAnnotation.options.literal as string; 46 | } 47 | 48 | // HttpQueries 49 | if ( 50 | t.typeName === "HttpQueries" || 51 | t.typeName === "HttpBody" || 52 | t.typeName === "HttpBodyValidation" 53 | ) { 54 | return this.getSchemaKey( 55 | ((t as RegistableSchema).typeArguments?.[0] ?? (t as RegistableSchema).originTypes?.[0]) as RegistableSchema, 56 | ); 57 | } 58 | 59 | if (this.customSchemaKeyFn) { 60 | const customName = this.customSchemaKeyFn(t); 61 | if (customName) return customName; 62 | } 63 | 64 | const rootName = 65 | t.kind === ReflectionKind.class ? t.classType.name : t.typeName ?? ""; 66 | 67 | const args = 68 | t.kind === ReflectionKind.class 69 | ? t.arguments ?? [] 70 | : t.typeArguments ?? []; 71 | 72 | return camelcase([rootName, ...args.map((a) => this.getTypeKey(a))], { 73 | pascalCase: true, 74 | }); 75 | } 76 | 77 | getTypeKey(t: Type): string { 78 | if ( 79 | t.kind === ReflectionKind.string || 80 | t.kind === ReflectionKind.number || 81 | t.kind === ReflectionKind.bigint || 82 | t.kind === ReflectionKind.boolean || 83 | t.kind === ReflectionKind.null || 84 | t.kind === ReflectionKind.undefined 85 | ) { 86 | return stringifyType(t); 87 | } else if ( 88 | t.kind === ReflectionKind.class || 89 | t.kind === ReflectionKind.objectLiteral || 90 | t.kind === ReflectionKind.enum || 91 | t.kind === ReflectionKind.union 92 | ) { 93 | return this.getSchemaKey(t); 94 | } else if (t.kind === ReflectionKind.array) { 95 | return camelcase([this.getTypeKey(t.type), "Array"], { 96 | pascalCase: false, 97 | }); 98 | } else { 99 | // Complex types not named 100 | return ""; 101 | } 102 | } 103 | 104 | registerSchema(name: string, type: Type, schema: Schema) { 105 | const currentEntry = this.store.get(name); 106 | 107 | if (currentEntry && !isSameType(type, currentEntry?.type)) { 108 | throw new DeepKitOpenApiSchemaNameConflict(type, currentEntry.type, name); 109 | } 110 | 111 | this.store.set(name, { 112 | type, 113 | name, 114 | schema: { 115 | ...schema, 116 | nullable: undefined 117 | } 118 | }); 119 | schema.__registryKey = name; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /packages/deepkit-openapi-core/src/ParametersResolver.ts: -------------------------------------------------------------------------------- 1 | import { ReflectionKind } from "@deepkit/type"; 2 | import { DeepKitOpenApiError, DeepKitTypeError } from "./errors"; 3 | import { SchemaRegistry } from "./SchemaRegistry"; 4 | import { MediaType, Parameter, ParsedRoute, RequestBody, RequestMediaTypeName } from "./types"; 5 | import { resolveTypeSchema } from "./TypeSchemaResolver"; 6 | 7 | export class ParametersResolver { 8 | parameters: Parameter[] = []; 9 | requestBody?: RequestBody; 10 | errors: DeepKitTypeError[] = []; 11 | 12 | constructor( 13 | private parsedRoute: ParsedRoute, 14 | private schemeRegistry: SchemaRegistry, 15 | private contentTypes?: RequestMediaTypeName[] 16 | ) {} 17 | 18 | resolve() { 19 | for (const parameter of this.parsedRoute.getParameters()) { 20 | const type = parameter.getType(); 21 | 22 | if (parameter.query) { 23 | const schemaResult = resolveTypeSchema(type, this.schemeRegistry); 24 | 25 | this.errors.push(...schemaResult.errors); 26 | 27 | this.parameters.push({ 28 | in: "query", 29 | name: parameter.getName(), 30 | schema: schemaResult.result, 31 | required: !parameter.parameter.isOptional(), 32 | }); 33 | } else if (parameter.queries) { 34 | if ( 35 | type.kind !== ReflectionKind.class && 36 | type.kind !== ReflectionKind.objectLiteral 37 | ) { 38 | throw new DeepKitOpenApiError( 39 | "HttpQueries should be either class or object literal. " 40 | ); 41 | } 42 | 43 | const schemaResult = resolveTypeSchema(type, this.schemeRegistry); 44 | 45 | this.errors.push(...schemaResult.errors); 46 | 47 | for (const [name, property] of Object.entries( 48 | schemaResult.result.properties! 49 | )) { 50 | if (!this.parameters.find((p) => p.name === name)) { 51 | this.parameters.push({ 52 | in: "query", 53 | name, 54 | schema: property, 55 | required: schemaResult.result.required?.includes(name), 56 | }); 57 | } else { 58 | this.errors.push( 59 | new DeepKitTypeError( 60 | `Parameter name ${JSON.stringify( 61 | name 62 | )} is repeated. Please consider renaming them. ` 63 | ) 64 | ); 65 | } 66 | } 67 | } else if (parameter.isPartOfPath()) { 68 | const schemaResult = resolveTypeSchema(type, this.schemeRegistry); 69 | 70 | this.errors.push(...schemaResult.errors); 71 | 72 | this.parameters.push({ 73 | in: "path", 74 | name: parameter.getName(), 75 | schema: schemaResult.result, 76 | required: true, 77 | }); 78 | } else if (parameter.body || parameter.bodyValidation) { 79 | if ( 80 | type.kind !== ReflectionKind.array && 81 | type.kind !== ReflectionKind.class && 82 | type.kind !== ReflectionKind.objectLiteral 83 | ) { 84 | throw new DeepKitOpenApiError( 85 | "HttpBody or HttpBodyValidation should be either array, class, or object literal." 86 | ); 87 | } 88 | 89 | const bodySchema = resolveTypeSchema(type, this.schemeRegistry); 90 | 91 | this.errors.push(...bodySchema.errors); 92 | 93 | const contentTypes = this.contentTypes ?? [ 94 | "application/json", 95 | "application/x-www-form-urlencoded", 96 | "multipart/form-data", 97 | ]; 98 | 99 | this.requestBody = { 100 | content: Object.fromEntries( 101 | contentTypes.map((contentType) => [ 102 | contentType, 103 | { 104 | schema: bodySchema.result, 105 | }, 106 | ]) 107 | ) as Record< 108 | RequestMediaTypeName, 109 | MediaType 110 | >, 111 | required: !parameter.parameter.isOptional(), 112 | }; 113 | } 114 | } 115 | 116 | return this; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /packages/deepkit-openapi-core/src/TypeSchemaResolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { Maximum, MaxLength, Minimum, MinLength, typeOf } from "@deepkit/type"; 2 | import { unwrapTypeSchema } from "./TypeSchemaResolver"; 3 | 4 | test("serialize atomic types", () => { 5 | expect(unwrapTypeSchema(typeOf())).toMatchObject({ 6 | __type: "schema", 7 | type: "string", 8 | }); 9 | 10 | expect(unwrapTypeSchema(typeOf>())).toMatchObject({ 11 | __type: "schema", 12 | type: "number", 13 | minLength: 5, 14 | }); 15 | 16 | expect(unwrapTypeSchema(typeOf>())).toMatchObject({ 17 | __type: "schema", 18 | type: "number", 19 | maxLength: 5, 20 | }); 21 | 22 | expect(unwrapTypeSchema(typeOf())).toMatchObject({ 23 | __type: "schema", 24 | type: "number", 25 | }); 26 | 27 | expect(unwrapTypeSchema(typeOf>())).toMatchObject({ 28 | __type: "schema", 29 | type: "number", 30 | minimum: 5, 31 | }); 32 | 33 | expect(unwrapTypeSchema(typeOf>())).toMatchObject({ 34 | __type: "schema", 35 | type: "number", 36 | maximum: 5, 37 | }); 38 | 39 | expect(unwrapTypeSchema(typeOf())).toMatchObject({ 40 | __type: "schema", 41 | type: "number", 42 | }); 43 | 44 | expect(unwrapTypeSchema(typeOf())).toMatchObject({ 45 | __type: "schema", 46 | type: "boolean", 47 | }); 48 | 49 | expect(unwrapTypeSchema(typeOf())).toMatchObject({ 50 | __type: "schema", 51 | nullable: true, 52 | }); 53 | }); 54 | 55 | test("serialize enum", () => { 56 | enum E1 { 57 | a = "a", 58 | b = "b", 59 | } 60 | 61 | expect(unwrapTypeSchema(typeOf())).toMatchObject({ 62 | __type: "schema", 63 | type: "string", 64 | enum: ["a", "b"], 65 | __registryKey: "E1", 66 | }); 67 | 68 | enum E2 { 69 | a = 1, 70 | b = 2, 71 | } 72 | 73 | expect(unwrapTypeSchema(typeOf())).toMatchObject({ 74 | __type: "schema", 75 | type: "number", 76 | enum: [1, 2], 77 | __registryKey: "E2", 78 | }); 79 | }); 80 | 81 | test("serialize union", () => { 82 | type Union = 83 | | { 84 | type: "push"; 85 | branch: string; 86 | } 87 | | { 88 | type: "commit"; 89 | diff: string[]; 90 | }; 91 | 92 | expect(unwrapTypeSchema(typeOf())).toMatchObject({ 93 | __type: "schema", 94 | oneOf: [ 95 | { 96 | __type: "schema", 97 | type: "object", 98 | properties: { 99 | type: { __type: "schema", type: "string", enum: ["push"] }, 100 | branch: { __type: "schema", type: "string" }, 101 | }, 102 | required: ["type", "branch"], 103 | }, 104 | { 105 | __type: "schema", 106 | type: "object", 107 | properties: { 108 | type: { __type: "schema", type: "string", enum: ["commit"] }, 109 | diff: { 110 | __type: "schema", 111 | type: "array", 112 | items: { __type: "schema", type: "string" }, 113 | }, 114 | }, 115 | required: ["type", "diff"], 116 | }, 117 | ], 118 | }); 119 | 120 | type EnumLike = "red" | "black"; 121 | 122 | expect(unwrapTypeSchema(typeOf())).toMatchObject({ 123 | __type: "schema", 124 | type: "string", 125 | enum: ["red", "black"], 126 | __registryKey: "EnumLike", 127 | }); 128 | }); 129 | 130 | test("serialize nullables", () => { 131 | const t1 = unwrapTypeSchema(typeOf()); 132 | expect(t1).toMatchObject({ 133 | __type: "schema", 134 | type: "string", 135 | }); 136 | expect(t1.nullable).toBeUndefined(); 137 | 138 | const t2 = unwrapTypeSchema(typeOf()); 139 | expect(t2).toMatchObject({ 140 | __type: "schema", 141 | type: "string", 142 | nullable: true, 143 | }); 144 | 145 | interface ITest { 146 | names: string[]; 147 | } 148 | const t3 = unwrapTypeSchema(typeOf()); 149 | expect(t3).toMatchObject({ 150 | __type: "schema", 151 | type: "object", 152 | }); 153 | expect(t3.nullable).toBeUndefined(); 154 | 155 | const t4 = unwrapTypeSchema(typeOf()); 156 | expect(t4).toMatchObject({ 157 | __type: "schema", 158 | type: "object", 159 | nullable: true, 160 | }); 161 | 162 | const t5 = unwrapTypeSchema(typeOf<'a' | 'b' | 'c'>()); 163 | expect(t5).toMatchObject({ 164 | __type: "schema", 165 | type: "string", 166 | enum: ["a", "b", "c"], 167 | }); 168 | expect(t5.nullable).toBeUndefined(); 169 | 170 | const t6 = unwrapTypeSchema(typeOf<'a' | 'b' | 'c' | null>()); 171 | expect(t6).toMatchObject({ 172 | __type: "schema", 173 | type: "string", 174 | enum: ["a", "b", "c"], 175 | nullable: true, 176 | }); 177 | }); -------------------------------------------------------------------------------- /packages/deepkit-openapi/src/module.ts: -------------------------------------------------------------------------------- 1 | import { createModuleClass } from "@deepkit/app"; 2 | import { 3 | HttpRequest, 4 | HttpResponse, 5 | HttpRouteFilter, 6 | normalizeDirectory, 7 | serveStaticListener, 8 | httpWorkflow, 9 | RouteConfig, 10 | } from "@deepkit/http"; 11 | import { urlJoin } from "@deepkit/core"; 12 | import { OpenAPIConfig } from "./module.config"; 13 | import { OpenAPIService } from "./service"; 14 | import { join, dirname } from "path"; 15 | import { stringify } from "yaml"; 16 | import { eventDispatcher } from "@deepkit/event"; 17 | import send from "send"; 18 | import { stat } from "fs/promises"; 19 | import { OpenAPI } from "deepkit-openapi-core"; 20 | 21 | export class OpenAPIModule extends createModuleClass({ 22 | config: OpenAPIConfig, 23 | providers: [OpenAPIService], 24 | exports: [OpenAPIService], 25 | }) { 26 | protected routeFilter = new HttpRouteFilter().excludeRoutes({ 27 | group: "app-static", 28 | }); 29 | 30 | protected configureOpenApiFunction: (openApi: OpenAPI) => void = () => {}; 31 | 32 | configureOpenApi(c: (openApi: OpenAPI) => void) { 33 | this.configureOpenApiFunction = c; 34 | return this; 35 | } 36 | 37 | configureHttpRouteFilter(c: (filter: HttpRouteFilter) => void) { 38 | c(this.routeFilter); 39 | return this; 40 | }; 41 | 42 | process() { 43 | this.addProvider({ provide: HttpRouteFilter, useValue: this.routeFilter }); 44 | 45 | const module = this; 46 | 47 | // Need to overwrite some static files provided by swagger-ui-dist 48 | class OpenApiStaticRewritingListener { 49 | constructor( 50 | private openApi: OpenAPIService, 51 | private config: OpenAPIConfig, 52 | ) {} 53 | 54 | serialize() { 55 | const openApi = this.openApi.serialize(); 56 | 57 | openApi.info.title = this.config.title; 58 | openApi.info.description = this.config.description; 59 | openApi.info.version = this.config.version; 60 | 61 | module.configureOpenApiFunction(openApi); 62 | return openApi; 63 | } 64 | 65 | get staticDirectory() { 66 | return dirname(require.resolve("swagger-ui-dist")); 67 | } 68 | 69 | get prefix() { 70 | return normalizeDirectory(this.config.prefix); 71 | } 72 | 73 | get swaggerInitializer() { 74 | return ` 75 | window.onload = function() { 76 | window.ui = SwaggerUIBundle({ 77 | url: ${JSON.stringify(this.prefix + "openapi.yml")}, 78 | dom_id: '#swagger-ui', 79 | deepLinking: true, 80 | presets: [ 81 | SwaggerUIBundle.presets.apis, 82 | SwaggerUIStandalonePreset 83 | ], 84 | plugins: [ 85 | SwaggerUIBundle.plugins.DownloadUrl 86 | ], 87 | layout: "StandaloneLayout" 88 | }); 89 | 90 | }; 91 | `; 92 | } 93 | 94 | serve(path: string, request: HttpRequest, response: HttpResponse) { 95 | if (path.endsWith("/swagger-initializer.js")) { 96 | response.setHeader( 97 | "content-type", 98 | "application/javascript; charset=utf-8", 99 | ); 100 | response.end(this.swaggerInitializer); 101 | } else if (path.endsWith("/openapi.json")) { 102 | const s = JSON.stringify(this.serialize(), undefined, 2); 103 | response.setHeader("content-type", "application/json; charset=utf-8"); 104 | response.end(s); 105 | } else if ( 106 | path.endsWith("/openapi.yaml") || 107 | path.endsWith("/openapi.yml") 108 | ) { 109 | const s = stringify(this.serialize(), { 110 | aliasDuplicateObjects: false, 111 | }); 112 | response.setHeader("content-type", "text/yaml; charset=utf-8"); 113 | response.end(s); 114 | } else { 115 | return new Promise(async (resolve, reject) => { 116 | const relativePath = urlJoin( 117 | "/", 118 | request.url!.substring(this.prefix.length), 119 | ); 120 | if (relativePath === "") { 121 | response.setHeader("location", this.prefix + "index.html"); 122 | response.status(301); 123 | return; 124 | } 125 | const finalLocalPath = join(this.staticDirectory, relativePath); 126 | 127 | const statResult = await stat(finalLocalPath); 128 | if (statResult.isFile()) { 129 | const res = send(request, path, { root: this.staticDirectory }); 130 | res.pipe(response); 131 | res.on("end", resolve); 132 | } else { 133 | response.write(`The static path ${request.url} is not found.`); 134 | response.status(404); 135 | } 136 | }); 137 | } 138 | } 139 | 140 | @eventDispatcher.listen(httpWorkflow.onRoute, 101) 141 | onRoute(event: typeof httpWorkflow.onRoute.event) { 142 | if (event.sent) return; 143 | if (event.route) return; 144 | 145 | if (!event.request.url?.startsWith(this.prefix)) return; 146 | 147 | const relativePath = urlJoin( 148 | "/", 149 | event.url.substring(this.prefix.length), 150 | ); 151 | 152 | event.routeFound( 153 | new RouteConfig("static", ["GET"], event.url, { 154 | type: 'controller', 155 | controller: OpenApiStaticRewritingListener, 156 | module, 157 | methodName: "serve", 158 | }), 159 | () => ({arguments: [relativePath, event.request, event.response], parameters: {}}), 160 | ); 161 | } 162 | } 163 | 164 | this.addListener(OpenApiStaticRewritingListener); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /packages/deepkit-openapi-core/src/OpenAPIDocument.ts: -------------------------------------------------------------------------------- 1 | import { ClassType } from "@deepkit/core"; 2 | import { parseRouteControllerAction, RouteClassControllerAction, RouteConfig } from "@deepkit/http"; 3 | import camelcase from "camelcase"; 4 | import { 5 | DeepKitOpenApiControllerNameConflict, 6 | DeepKitOpenApiOperationNameConflict, 7 | DeepKitTypeError, 8 | } from "./errors"; 9 | import { ParametersResolver } from "./ParametersResolver"; 10 | import { SchemaKeyFn, SchemaRegistry } from "./SchemaRegistry"; 11 | import { 12 | Schema, 13 | HttpMethod, 14 | OpenAPI, 15 | OpenAPIResponse, 16 | Operation, 17 | ParsedRoute, 18 | Tag, 19 | Responses, 20 | RequestMediaTypeName, 21 | } from "./types"; 22 | import cloneDeepWith from "lodash.clonedeepwith"; 23 | import { resolveOpenApiPath } from "./utils"; 24 | import { resolveTypeSchema } from "./TypeSchemaResolver"; 25 | import { ReflectionKind } from "@deepkit/type"; 26 | import { ScopedLogger } from "@deepkit/logger"; 27 | 28 | export class OpenAPICoreConfig { 29 | customSchemaKeyFn?: SchemaKeyFn; 30 | contentTypes?: RequestMediaTypeName[]; 31 | } 32 | 33 | export class OpenAPIDocument { 34 | schemaRegistry = new SchemaRegistry(this.config.customSchemaKeyFn); 35 | 36 | operations: Operation[] = []; 37 | 38 | tags: Tag[] = []; 39 | 40 | errors: DeepKitTypeError[] = []; 41 | 42 | constructor(private routes: RouteConfig[], private log: ScopedLogger, private config: OpenAPICoreConfig = {}) {} 43 | 44 | getControllerName(controller: ClassType) { 45 | // TODO: Allow customized name 46 | return camelcase(controller.name.replace(/Controller$/, "")); 47 | } 48 | 49 | registerTag(controller: ClassType) { 50 | const name = this.getControllerName(controller); 51 | const newTag = { 52 | __controller: controller, 53 | name, 54 | }; 55 | const currentTag = this.tags.find((tag) => tag.name === name); 56 | if (currentTag) { 57 | if (currentTag.__controller !== controller) { 58 | throw new DeepKitOpenApiControllerNameConflict( 59 | controller, 60 | currentTag.__controller, 61 | name, 62 | ); 63 | } 64 | } else { 65 | this.tags.push(newTag); 66 | } 67 | 68 | return newTag; 69 | } 70 | 71 | getDocument(): OpenAPI { 72 | for (const route of this.routes) { 73 | this.registerRouteSafe(route); 74 | } 75 | 76 | const openapi: OpenAPI = { 77 | openapi: "3.0.3", 78 | info: { 79 | title: "OpenAPI", 80 | contact: {}, 81 | license: { name: "MIT" }, 82 | version: "0.0.1", 83 | }, 84 | servers: [], 85 | paths: {}, 86 | components: {}, 87 | }; 88 | 89 | for (const operation of this.operations) { 90 | const openApiPath = resolveOpenApiPath(operation.__path); 91 | 92 | if (!openapi.paths[openApiPath]) { 93 | openapi.paths[openApiPath] = {}; 94 | } 95 | openapi.paths[openApiPath][operation.__method as HttpMethod] = operation; 96 | } 97 | 98 | for (const [key, schema] of this.schemaRegistry.store) { 99 | openapi.components.schemas = openapi.components.schemas ?? {}; 100 | openapi.components.schemas[key] = { 101 | ...schema.schema, 102 | __isComponent: true, 103 | }; 104 | } 105 | 106 | return openapi; 107 | } 108 | 109 | serializeDocument(): OpenAPI { 110 | return cloneDeepWith(this.getDocument(), (c) => { 111 | if (c && typeof c === "object") { 112 | if (c.__type === "schema" && c.__registryKey && !c.__isComponent) { 113 | const ret = { 114 | $ref: `#/components/schemas/${c.__registryKey}`, 115 | }; 116 | 117 | if (c.nullable) { 118 | return { 119 | nullable: true, 120 | allOf: [ret], 121 | }; 122 | } 123 | 124 | return ret; 125 | } 126 | 127 | for (const key of Object.keys(c)) { 128 | // Remove internal keys. 129 | if (key.startsWith("__")) delete c[key]; 130 | } 131 | } 132 | }); 133 | } 134 | 135 | registerRouteSafe(route: RouteConfig) { 136 | try { 137 | this.registerRoute(route); 138 | } catch (err: any) { 139 | this.log.error(`Failed to register route ${route.httpMethods.join(',')} ${route.getFullPath()}`, err); 140 | } 141 | } 142 | 143 | registerRoute(route: RouteConfig) { 144 | if (route.action.type !== 'controller') { 145 | throw new Error('Sorry, only controller routes are currently supported!'); 146 | } 147 | 148 | const controller = route.action.controller; 149 | const tag = this.registerTag(controller); 150 | const parsedRoute = parseRouteControllerAction(route); 151 | 152 | for (const method of route.httpMethods) { 153 | const parametersResolver = new ParametersResolver( 154 | parsedRoute, 155 | this.schemaRegistry, 156 | this.config.contentTypes 157 | ).resolve(); 158 | this.errors.push(...parametersResolver.errors); 159 | 160 | const responses = this.resolveResponses(route); 161 | 162 | if (route.action.type !== 'controller') { 163 | throw new Error('Sorry, only controller routes are currently supported!'); 164 | } 165 | 166 | const slash = route.path.length === 0 || route.path.startsWith('/') ? '' : '/'; 167 | 168 | const operation: Operation = { 169 | __path: `${route.baseUrl}${slash}${route.path}`, 170 | __method: method.toLowerCase(), 171 | tags: [tag.name], 172 | operationId: camelcase([method, tag.name, route.action.methodName]), 173 | parameters: 174 | parametersResolver.parameters.length > 0 175 | ? parametersResolver.parameters 176 | : undefined, 177 | requestBody: parametersResolver.requestBody, 178 | responses, 179 | }; 180 | 181 | if ( 182 | this.operations.find( 183 | (p) => p.__path === operation.__path && p.__method === operation.__method, 184 | ) 185 | ) { 186 | throw new DeepKitOpenApiOperationNameConflict( 187 | operation.__path, 188 | operation.__method, 189 | ); 190 | } 191 | 192 | this.operations.push(operation); 193 | } 194 | } 195 | 196 | resolveResponses(route: RouteConfig) { 197 | const responses: Responses = {}; 198 | 199 | // First get the response type of the method 200 | if (route.returnType) { 201 | const schemaResult = resolveTypeSchema( 202 | route.returnType.kind === ReflectionKind.promise 203 | ? route.returnType.type 204 | : route.returnType, 205 | this.schemaRegistry, 206 | ); 207 | 208 | this.errors.push(...schemaResult.errors); 209 | 210 | responses[200] = { 211 | description: "", 212 | content: { 213 | "application/json": { 214 | schema: schemaResult.result, 215 | }, 216 | }, 217 | }; 218 | } 219 | 220 | // Annotated responses have higher priority 221 | for (const response of route.responses) { 222 | let schema: Schema | undefined; 223 | if (response.type) { 224 | const schemaResult = resolveTypeSchema( 225 | response.type, 226 | this.schemaRegistry, 227 | ); 228 | schema = schemaResult.result; 229 | this.errors.push(...schemaResult.errors); 230 | } 231 | 232 | if (!responses[response.statusCode]) { 233 | responses[response.statusCode] = { 234 | description: "", 235 | content: { "application/json": schema ? { schema } : undefined }, 236 | }; 237 | } 238 | 239 | responses[response.statusCode].description ||= response.description; 240 | if (schema) { 241 | responses[response.statusCode].content["application/json"]!.schema = 242 | schema; 243 | } 244 | } 245 | 246 | return responses; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /packages/deepkit-openapi-core/src/TypeSchemaResolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DeepKitTypeError, 3 | DeepKitTypeErrors, 4 | LiteralSupported, 5 | TypeNotSupported, 6 | } from "./errors"; 7 | import { AnySchema, Schema } from "./types"; 8 | 9 | import { 10 | isDateType, 11 | reflect, 12 | ReflectionKind, 13 | stringifyType, 14 | Type, 15 | TypeClass, 16 | TypeEnum, 17 | TypeLiteral, 18 | TypeObjectLiteral, 19 | typeOf, 20 | validationAnnotation, 21 | } from "@deepkit/type"; 22 | import { validators } from "./validators"; 23 | import { getParentClass } from "@deepkit/core"; 24 | import { SchemaRegistry } from "./SchemaRegistry"; 25 | 26 | export class TypeSchemaResolver { 27 | result: Schema = { ...AnySchema }; 28 | errors: DeepKitTypeError[] = []; 29 | 30 | constructor(public t: Type, public schemaRegisty: SchemaRegistry) {} 31 | 32 | resolveBasic() { 33 | switch (this.t.kind) { 34 | case ReflectionKind.never: 35 | this.result.not = AnySchema; 36 | return; 37 | case ReflectionKind.any: 38 | case ReflectionKind.unknown: 39 | case ReflectionKind.void: 40 | this.result = AnySchema; 41 | return; 42 | case ReflectionKind.object: 43 | this.result.type = "object"; 44 | return; 45 | case ReflectionKind.string: 46 | this.result.type = "string"; 47 | return; 48 | case ReflectionKind.number: 49 | this.result.type = "number"; 50 | return; 51 | case ReflectionKind.boolean: 52 | this.result.type = "boolean"; 53 | return; 54 | case ReflectionKind.bigint: 55 | this.result.type = "number"; 56 | return; 57 | case ReflectionKind.null: 58 | this.result.nullable = true; 59 | return; 60 | case ReflectionKind.undefined: 61 | this.result.__isUndefined = true; 62 | return; 63 | case ReflectionKind.literal: 64 | const type = mapSimpleLiteralToType(this.t.literal); 65 | if (type) { 66 | this.result.type = type; 67 | this.result.enum = [this.t.literal as any]; 68 | } else { 69 | this.errors.push(new LiteralSupported(typeof this.t.literal)); 70 | } 71 | 72 | return; 73 | case ReflectionKind.templateLiteral: 74 | this.result.type = "string"; 75 | this.errors.push( 76 | new TypeNotSupported( 77 | this.t, 78 | "Literal is treated as string for simplicity", 79 | ), 80 | ); 81 | 82 | return; 83 | case ReflectionKind.class: 84 | case ReflectionKind.objectLiteral: 85 | this.resolveClassOrObjectLiteral(); 86 | return; 87 | case ReflectionKind.array: 88 | this.result.type = "array"; 89 | const itemsResult = resolveTypeSchema(this.t.type, this.schemaRegisty); 90 | 91 | this.result.items = itemsResult.result; 92 | this.errors.push(...itemsResult.errors); 93 | return; 94 | case ReflectionKind.enum: 95 | this.resolveEnum(); 96 | return; 97 | case ReflectionKind.union: 98 | this.resolveUnion(); 99 | return; 100 | default: 101 | this.errors.push(new TypeNotSupported(this.t)); 102 | return; 103 | } 104 | } 105 | 106 | resolveClassOrObjectLiteral() { 107 | if ( 108 | this.t.kind !== ReflectionKind.class && 109 | this.t.kind !== ReflectionKind.objectLiteral 110 | ) { 111 | return; 112 | } 113 | 114 | // Dates will be serialized to string 115 | if (isDateType(this.t)) { 116 | this.result.type = "string"; 117 | return; 118 | } 119 | 120 | this.result.type = "object"; 121 | 122 | let typeClass: TypeClass | TypeObjectLiteral | undefined = this.t; 123 | this.result.properties = {}; 124 | 125 | const typeClasses: (TypeClass | TypeObjectLiteral | undefined)[] = [this.t]; 126 | 127 | const required: string[] = []; 128 | 129 | if (this.t.kind === ReflectionKind.class) { 130 | // Build a list of inheritance, from root to current class. 131 | while (true) { 132 | const parentClass = getParentClass((typeClass as TypeClass).classType); 133 | if (parentClass) { 134 | typeClass = reflect(parentClass) as any; 135 | typeClasses.unshift(typeClass); 136 | } else { 137 | break; 138 | } 139 | } 140 | } 141 | 142 | // Follow the order to override properties. 143 | for (const typeClass of typeClasses) { 144 | for (const typeItem of typeClass!.types) { 145 | if ( 146 | typeItem.kind === ReflectionKind.property || 147 | typeItem.kind === ReflectionKind.propertySignature 148 | ) { 149 | const typeResolver = resolveTypeSchema( 150 | typeItem.type, 151 | this.schemaRegisty, 152 | ); 153 | 154 | if (!typeItem.optional && !required.includes(String(typeItem.name))) { 155 | required.push(String(typeItem.name)); 156 | } 157 | 158 | this.result.properties[String(typeItem.name)] = typeResolver.result; 159 | this.errors.push(...typeResolver.errors); 160 | } 161 | } 162 | } 163 | 164 | if (required.length) { 165 | this.result.required = required; 166 | } 167 | 168 | const registryKey = this.schemaRegisty.getSchemaKey(this.t); 169 | 170 | if (registryKey) { 171 | this.schemaRegisty.registerSchema(registryKey, this.t, this.result); 172 | } 173 | } 174 | 175 | resolveEnum() { 176 | if (this.t.kind !== ReflectionKind.enum) { 177 | return; 178 | } 179 | 180 | let types = new Set(); 181 | 182 | for (const value of this.t.values) { 183 | const currentType = mapSimpleLiteralToType(value); 184 | 185 | if (currentType === undefined) { 186 | this.errors.push( 187 | new TypeNotSupported(this.t, `Enum with unsupported members. `), 188 | ); 189 | continue; 190 | } 191 | 192 | types.add(currentType); 193 | } 194 | 195 | this.result.type = types.size > 1 ? undefined : [...types.values()][0]; 196 | this.result.enum = this.t.values as any; 197 | 198 | const registryKey = this.schemaRegisty.getSchemaKey(this.t); 199 | if (registryKey) { 200 | this.schemaRegisty.registerSchema(registryKey, this.t, this.result); 201 | } 202 | } 203 | 204 | resolveUnion() { 205 | if (this.t.kind !== ReflectionKind.union) { 206 | return; 207 | } 208 | 209 | const hasNull = this.t.types.some((t) => t.kind === ReflectionKind.null); 210 | if (hasNull) { 211 | this.result.nullable = true; 212 | this.t = { ...this.t, types: this.t.types.filter((t) => t.kind !== ReflectionKind.null) }; 213 | } 214 | 215 | // if there's only one type left in the union, pull it up a level and go back to resolveBasic 216 | if (this.t.types.length === 1) { 217 | this.t = this.t.types[0]; 218 | return this.resolveBasic(); 219 | } 220 | 221 | // Find out whether it is a union of literals. If so, treat it as an enum 222 | if ( 223 | this.t.types.every( 224 | (t): t is TypeLiteral => 225 | t.kind === ReflectionKind.literal && 226 | ["string", "number"].includes(mapSimpleLiteralToType(t.literal) as any), 227 | ) 228 | ) { 229 | const enumType: TypeEnum = { 230 | ...this.t, 231 | kind: ReflectionKind.enum, 232 | enum: Object.fromEntries( 233 | this.t.types.map((t) => [t.literal, t.literal as any]), 234 | ), 235 | values: this.t.types.map((t) => t.literal as any), 236 | indexType: this.t, 237 | }; 238 | 239 | const { result, errors } = resolveTypeSchema(enumType, this.schemaRegisty); 240 | this.result = result; 241 | this.errors.push(...errors); 242 | if (hasNull) { 243 | this.result.enum!.push(null); 244 | this.result.nullable = true; 245 | } 246 | return; 247 | } 248 | 249 | this.result.type = undefined; 250 | this.result.oneOf = []; 251 | 252 | for (const t of this.t.types) { 253 | const { result, errors } = resolveTypeSchema(t, this.schemaRegisty); 254 | this.result.oneOf?.push(result); 255 | this.errors.push(...errors); 256 | } 257 | } 258 | 259 | resolveValidators() { 260 | for (const annotation of validationAnnotation.getAnnotations(this.t)) { 261 | const { name, args } = annotation; 262 | 263 | const validator = validators[name]; 264 | 265 | if (!validator) { 266 | this.errors.push( 267 | new TypeNotSupported(this.t, `Validator ${name} is not supported. `), 268 | ); 269 | } else { 270 | try { 271 | this.result = validator(this.result, ...(args as [any])); 272 | } catch (e) { 273 | if (e instanceof TypeNotSupported) { 274 | this.errors.push(e); 275 | } else { 276 | throw e; 277 | } 278 | } 279 | } 280 | } 281 | } 282 | 283 | resolve() { 284 | this.resolveBasic(); 285 | this.resolveValidators(); 286 | 287 | return this; 288 | } 289 | } 290 | 291 | export const mapSimpleLiteralToType = (literal: any) => { 292 | if (typeof literal === "string") { 293 | return "string"; 294 | } else if (typeof literal === "bigint") { 295 | return "integer"; 296 | } else if (typeof literal === "number") { 297 | return "number"; 298 | } else if (typeof literal === "boolean") { 299 | return "boolean"; 300 | } else { 301 | return; 302 | } 303 | }; 304 | 305 | export const unwrapTypeSchema = ( 306 | t: Type, 307 | r: SchemaRegistry = new SchemaRegistry(), 308 | ) => { 309 | const resolver = new TypeSchemaResolver(t, new SchemaRegistry()).resolve(); 310 | 311 | if (resolver.errors.length === 0) { 312 | return resolver.result; 313 | } else { 314 | throw new DeepKitTypeErrors(resolver.errors, "Errors with input type. "); 315 | } 316 | }; 317 | 318 | export const resolveTypeSchema = ( 319 | t: Type, 320 | r: SchemaRegistry = new SchemaRegistry(), 321 | ) => { 322 | return new TypeSchemaResolver(t, r).resolve(); 323 | }; 324 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![deepkit-openapi](https://img.shields.io/badge/-deepkit--openapi-green)](https://www.npmjs.com/package/deepkit-openapi) [![npm](https://img.shields.io/npm/v/deepkit-openapi)](https://www.npmjs.com/package/deepkit-openapi) [![deepkit-openapi](https://img.shields.io/badge/-deepkit--openapi--core-darkgreen)](https://www.npmjs.com/package/deepkit-openapi-core) [![npm](https://img.shields.io/npm/v/deepkit-openapi-core)](https://www.npmjs.com/package/deepkit-openapi-core) [![ts](https://img.shields.io/badge/-typescript-%20%233078c6)](https://www.typescriptlang.org/) [![deepkit](https://img.shields.io/badge/-deepkit-black)](https://deepkit.io/) 2 | # Deepkit OpenAPI 3 | 4 |
5 | 6 |

7 | 8 |

9 | 10 |

11 | from types, to types 12 |

13 | 14 | 15 | 16 | ## Introduction 17 | 18 | :warning: THIS LIBRARY IS UNDER 1.0.0 :warning: 19 | 20 | This is a [Deepkit Framework](https://deepkit.io/framework) module for automatically generating [OpenAPI V3](https://swagger.io/specification/) definitions. 21 | 22 | If you wonder what Deepkit is, it is a TypeScript framework that adds [reflection](https://en.wikipedia.org/wiki/Reflective_programming) functionalities to the already great TS language. With Deepkit, the user exploits the richness of TypeScript syntax to define solid API interface, painless validation and runtime self checks. Learn more at its [official site](https://deepkit.io/framework). 23 | 24 | OpenAPI, on the other hand, is a popular schema for HTTP API definitions. There are countless serverside frameworks supporting it, like Java Spring, Django, FastAPI and NestJS, to name a few. However, it is Deepkit who has the potential to map code to OpenAPI definition with the least repeated code. Deepkit provides the ability to have TypeScript's static types in runtime, therefore no more decorators like `@ApiProperty` are needed. Also, Deepkit inherits the rich and rigid type system of TypeScript, with a battle-tested type checker `tsc` and the wide typed JavaScript ecosystem, so it should provide more type safety than Python 3.5+. 25 | 26 | ## Get Started 27 | 28 | *Warning: This package is intended to only work with deepkit* 29 | 30 | ### Install Deepkit Openapi 31 | 32 | npm 33 | 34 | ```bash 35 | npm install deepkit-openapi 36 | ``` 37 | 38 | yarn 39 | 40 | ```bash 41 | yarn add deepkit-openapi 42 | ``` 43 | 44 | pnpm 45 | 46 | ```bash 47 | pnpm add deepkit-openapi 48 | ``` 49 | 50 | ### Import OpenAPIModule 51 | 52 | ```ts 53 | import { FrameworkModule } from "@deepkit/framework"; 54 | import { OpenAPIModule } from "deepkit-openapi"; 55 | 56 | // ... 57 | 58 | @http.controller() 59 | class UserController { 60 | // ... 61 | } 62 | 63 | new App({ 64 | providers: [UserController], 65 | controllers: [UserController], 66 | imports: [ 67 | new OpenAPIModule({ prefix: "/openapi/" }), // Import the module here 68 | new FrameworkModule({ 69 | publicDir: "public", 70 | httpLog: true, 71 | migrateOnStartup: true, 72 | }), 73 | ], 74 | }) 75 | .loadConfigFromEnv() 76 | .run(); 77 | 78 | ``` 79 | 80 | ### Start the Application 81 | 82 | ```bash 83 | ts-node app.ts server:start 84 | 85 | 2022-06-04T15:14:57.744Z [LOG] Start server ... 86 | 2022-06-04T15:14:57.747Z [LOG] 4 HTTP routes 87 | 2022-06-04T15:14:57.747Z [LOG] HTTP Controller UserController 88 | 2022-06-04T15:14:57.747Z [LOG] GET /user/:id 89 | 2022-06-04T15:14:57.747Z [LOG] POST /user/ 90 | 2022-06-04T15:14:57.747Z [LOG] PATCH /user/:id 91 | 2022-06-04T15:14:57.747Z [LOG] DELETE /user/:id 92 | 2022-06-04T15:14:57.747Z [LOG] HTTP listening at http://0.0.0.0:8080 93 | 2022-06-04T15:14:57.747Z [LOG] Server started. 94 | ``` 95 | 96 | Visit http://localhost:8080/openapi/index.html 97 | 98 | ![](packages/docs/2022-06-05-00-21-04.png) 99 | 100 | Now you get your OpenAPI documentation up and running with a single line of code! No annotations needed! 101 | 102 | ```ts 103 | new OpenAPIModule({ prefix: "/openapi/" }) 104 | ``` 105 | 106 | ## Tutorial 107 | 108 | ### Introducing OpenAPIModule 109 | 110 | To use `deepkit-openapi`, first we need to import `OpenAPIModule` from `deepkit-openapi`. `OpenAPIModule` is an `AppModule` that works like a custom [deepkit module](https://deepkit.io/documentation/framework/modules). 111 | 112 | ```ts 113 | import { OpenAPIModule } from "deepkit-openapi"; 114 | ``` 115 | 116 | To make this module effective, add it to your `App`'s import: 117 | 118 | ```ts 119 | new App({ 120 | imports: [ 121 | new OpenAPIModule(), 122 | new FrameworkModule(), 123 | ], 124 | }).run(); 125 | ``` 126 | 127 | The `OpenAPIModule` does two things: 128 | 129 | 1. Serve `openapi.yml`, `openapi.json` under the root path, by default `/openapi`. When the query comes, it builds the OpenAPI document on the fly, according to current working HTTP controllers. 130 | 2. Serve the Swagger UI static site at `index.html`, which loads the `openapi.yml`. 131 | 132 | Basically, `OpenAPiModule` builds the OpenAPI document lazily, according to the running controllers. Furthur works should be allowing the user to cache the generated document. It also serves Swagger UI, which includes exactly the files of `swagger-ui-dist`, for your convenience. You can use your own OpenAPI loader, of course. 133 | 134 | To customize `OpenAPIModule`, provide a `OpenAPIConfig` as the parameter of `new OpenAPIModule()`. You can provide your own values optionally to it, allowing you to customize its functions. 135 | 136 | ```ts 137 | import { OpenAPIModule, OpenAPIConfig } from 'deepkit-openapi'; 138 | 139 | new OpenAPIModule({ 140 | title: 'The title of your APIs', // default "OpenAPI" 141 | description: 'The description of your APIs', // default "" 142 | version: 'x.y.z', // The version of your APIs. default "1.0.0" 143 | prefix: '/url-prefix', // The prefix of all OpenAPIModule controllers. 144 | }); 145 | ``` 146 | 147 | To further configure `OpenAPIModule`, call the following chainable methods of `OpenAPIModule`: 148 | 149 | ```ts 150 | configureOpenApi(c: (openApi: OpenAPI) => void): OpenAPIModule 151 | ``` 152 | 153 | Manipulate the OpenAPI document after it is generated. You can mutate the document in any way you like, especially for those functions currently we don't support. 154 | 155 | ```ts 156 | configureHttpRouteFilter(c: (filter: HttpRouteFilter) => void): OpenAPIModule 157 | ``` 158 | 159 | Configure the `HttpRouteFilter` you are using, which allows you to include or exclude any paths you want. View its type to learn how to use it. 160 | 161 | ### Define controllers 162 | 163 | In most cases, `deepkit-openapi` just works for your existing project. However, there are limitations of `deepkit` and `deepkit-openapi` that limit your way of writing codes. 164 | 165 | You need to define your controllers using `@http.controller()` and `@http.GET` or `@http.`. 166 | 167 | ```ts 168 | @http.controller() 169 | class UserController { 170 | @http.GET("/user/:id").response(200, "Read a User by its ID") 171 | read(id: number) { 172 | return db.find((user) => user.id === id); 173 | } 174 | } 175 | ``` 176 | 177 | The code above maps to the following OpenAPI: 178 | 179 | ```yml 180 | # ... 181 | paths: 182 | "/user/{id}": 183 | get: 184 | tags: 185 | - user 186 | operationId: getUserRead 187 | parameters: 188 | - in: path 189 | name: id 190 | schema: 191 | type: number 192 | required: true 193 | responses: 194 | "200": 195 | description: Read a User by its ID 196 | content: 197 | application/json: 198 | schema: 199 | $ref: "#/components/schemas/ReadUser" 200 | components: 201 | schemas: 202 | ReadUser: 203 | type: object 204 | properties: 205 | id: 206 | type: number 207 | name: 208 | type: string 209 | email: 210 | type: string 211 | required: 212 | - id 213 | - name 214 | - email 215 | ``` 216 | 217 | Please pay attention to how each part of the controller correponds to the generated document: 218 | 219 | 1. You need to specify the return type in `response` decorator. In fact, you can also specify it by explictly marking the return type. However, the `response` has higher priority, since if both exist, the returning value will be converted to the type in the decorator. Note that this typing is by-nature unsafe because any errors could return other than your given response. 220 | 2. The parameters in query, body and path are automatically documented. `HttpQueries` are supported so you can reuse grouped queries. Currently, there are no ways to document header parameters. 221 | 3. The HTTP method name, controller name and handler method name makes up the `operationId`. 222 | 4. All interfaces or classes used in the schema will be kept and reused in `components.schemas`. You are responsible for keeping their names unique. The name is generated based on the type name and its concrete type arguments. 223 | 5. Since deepkit doesn't type the `content-type` yet, we *do not* support response and request types other than `application/json`. 224 | 225 | ### Supported types 226 | 227 | The following types are supported: 228 | 229 | 1. never 230 | 2. any 231 | 3. unknown 232 | 4. void 233 | 5. object (arbitary objects) 234 | 6. string 235 | 7. number 236 | 8. boolean 237 | 9. bigint 238 | 10. null 239 | 11. undefined 240 | 12. literal 241 | 1. basic literals are mapped to single-valued enums. 242 | 13. template literal 243 | 1. just mapped to string 244 | 14. class and interface 245 | 15. array 246 | 16. enum 247 | 17. union 248 | 249 | Types not supported yet: 250 | 1. Special types that are mapped to basic types by `serializer`, like `Date`. 251 | 252 | Types *not* planned to support: 253 | 1. Generic types 254 | 2. Intersection types. Use inheritance instead. 255 | 3. Regex 256 | 4. Functions 257 | 5. Other types that make no sense once serialized. 258 | 259 | ### Define your API types in TypeScript 260 | 261 | With deepkit, you can enjoy the simplicity of manipulating your types for validation: 262 | 263 | For example, you have a `User`, now you want `CreateUser` for user sign up, and `ReadUser` for reading users. In a simple settings, we have following types that work well with plain deepkit. 264 | 265 | ```ts 266 | interface User { 267 | id: number; 268 | name: string; 269 | password: string; 270 | } 271 | 272 | type ReadUser = Omit; 273 | 274 | type CreateUser = Omit; 275 | ``` 276 | 277 | The example above gives us the following OpenAPI schemas: 278 | 279 | ```yml 280 | components: 281 | schemas: 282 | ReadUser: 283 | type: object 284 | properties: 285 | id: 286 | type: number 287 | name: 288 | type: string 289 | required: 290 | - id 291 | - name 292 | CreateUser: 293 | type: object 294 | properties: 295 | name: 296 | type: string 297 | password: 298 | type: string 299 | required: 300 | - name 301 | - password 302 | User: 303 | type: object 304 | properties: 305 | id: 306 | type: number 307 | name: 308 | type: string 309 | password: 310 | type: string 311 | required: 312 | - id 313 | - name 314 | - password 315 | ``` 316 | 317 | ## Contributing 318 | 319 | Thank you for reading! If you want to contribute to this project, take a look at [DEVELOPMENT.md](DEVELOPMENT.md). This is a monorepo project based on yarn@1 and deepkit's compiler. Note that deepkit and the deepkit community is still young, and any changes might break this library. Use it and devote yourself at your own risk! 320 | --------------------------------------------------------------------------------