├── .gitignore ├── README.md ├── airship.png ├── bin ├── aschemegen ├── asdkgen └── asdocgen ├── package.json ├── src ├── index.ts └── modules │ ├── apiServer │ ├── application │ │ └── AirshipAPIServer.ts │ ├── domain │ │ ├── BaseRequestHandler.ts │ │ ├── RequestsProvider.ts │ │ ├── ServerConfig.ts │ │ └── entity │ │ │ ├── ASErrorResponse.ts │ │ │ ├── ASRequest.ts │ │ │ ├── ASResponse.ts │ │ │ └── ASSuccessResponse.ts │ └── infrastructure │ │ ├── HttpRequestsProvider.ts │ │ ├── MultyRequestHandler.ts │ │ └── RequestHandlersManager.ts │ ├── cache │ ├── domain │ │ └── BaseCache.ts │ └── infrustructure │ │ └── MemoryCache.ts │ ├── codeGen │ ├── domain │ │ ├── CodeGenerator.ts │ │ ├── CodeLine.ts │ │ ├── SourceCode.ts │ │ ├── schema │ │ │ ├── ApiMethodParam.ts │ │ │ ├── ApiMethodScheme.ts │ │ │ ├── ClassField.ts │ │ │ └── ClassScheme.ts │ │ └── types │ │ │ ├── AnyType.ts │ │ │ ├── BooleanType.ts │ │ │ ├── CustomType.ts │ │ │ ├── IntBoolType.ts │ │ │ ├── NumberType.ts │ │ │ ├── ObjectType.ts │ │ │ ├── StringType.ts │ │ │ ├── Type.ts │ │ │ └── VectorType.ts │ └── infrastructure │ │ ├── JavaScriptCodeGenerator.ts │ │ ├── SwiftCodeGenerator.ts │ │ ├── TypescriptCodeGenerator.ts │ │ └── Utils.ts │ ├── doc │ ├── domain │ │ └── BaseApiDocGenerator.ts │ ├── infrastructure │ │ └── ApiDocGenerator.ts │ └── presentation │ │ └── DocsGeneratorApplication.ts │ ├── logger │ ├── domain │ │ └── BaseLogger.ts │ └── infrustructure │ │ └── ConsoleLogger.ts │ ├── schemeGenerator │ ├── application │ │ └── AirshipSchemeGenerator.ts │ ├── domain │ │ ├── ApiSchema.ts │ │ └── ApiSchemeGenerator.ts │ ├── infrastructure │ │ └── AirshipApiSchemeGenerator.ts │ └── presentation │ │ └── AirshipSchemeGeneratorApplication.ts │ ├── sdkGenerator │ ├── application │ │ └── AirshipSDKGenerator.ts │ ├── domain │ │ ├── ApiSDKGenerator.ts │ │ ├── SDKConfig.ts │ │ └── SDKFile.ts │ ├── infrastructure │ │ └── AirshipApiSDKGenerator.ts │ └── presentation │ │ └── AirshipSDKGeneratorApplication.ts │ ├── serialize │ ├── BaseSerializer.ts │ └── JSONSerializer.ts │ ├── statistics │ ├── domain │ │ └── BaseStatisticsCounter.ts │ └── infrastructure │ │ └── LocalStatisticsCounter.ts │ └── utils │ ├── BaseConsoleApplication.ts │ └── CallbackQueue.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .idea 4 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 |

5 | 6 | # Airship 7 | 8 | # Introduction 9 | Airship is a framework for Node.JS & TypeScript that helps you to write big, scalable and maintainable API servers. 10 | 11 | Main features: 12 | 13 | - clean and simple architecture 14 | - statically typed 15 | - simple API 16 | - automatic models serialization and deserialization 17 | - automatic API Scheme generation 18 | - automatic SDK generation for the web frontend 19 | - automatic documentation generation 20 | - ability to switch transport protocol (at this moment only HTTP is implemented) 21 | 22 | # Basic concepts 23 | The main idea is very simple, every request has its model (its just a class), every response has a model too. To handle request there is a component called `RequestHandler`, every request handler handles a specific request and returns specific response. Its important that at this point whole system does not even know anything about the network and it should not. Because of that, your system is very abstract, it just handles specified requests and returns specified responses. This gives you the ability to change your network protocol or even stop using your system like a web server and use it as a part of local UI application. 24 | 25 | ## Installation 26 | 27 | To install the stable version: 28 | 29 | ```bash 30 | npm install --save airship-server 31 | ``` 32 | 33 | This assumes you are using [npm](https://www.npmjs.com/) as your package manager. 34 | If you don’t, you can access these files on [unpkg](https://unpkg.com/airship-server/), download them, or point your package manager to them. 35 | 36 | 37 | # Basic example 38 | Let's imagine that we need a web server with just one method `/randomInt` which returns a random integer in range. 39 | 40 | First of all, we need to write our request and response models: 41 | 42 | ```ts 43 | @queryPath('/getRandomInt') 44 | export class RandomIntRequest extends ASRequest { 45 | @serializable() 46 | public readonly min: number 47 | 48 | @serializable() 49 | public readonly max: number 50 | 51 | constructor( 52 | min: number, 53 | max: number 54 | ) { 55 | super() 56 | 57 | this.min = min 58 | this.max = max 59 | } 60 | } 61 | ``` 62 | 63 | So, it's pretty simple, we just created a class that extends from base request class and have `from` and `to` properties. The only interesting thing is using of `@serializable()` decorator, it's used to have the ability to serialize and deserialize our model. 64 | And we also setting query path using `@queryPath` decorator, that string will be used for URL in HTTP & for methods names in SDK 65 | 66 | 67 | 68 | Response model is also very simple and extends from base response model. 69 | 70 | ```ts 71 | export class RandomIntResponse extends ASResponse { 72 | @serializable() 73 | public readonly integer: number 74 | 75 | constructor(integer: number) { 76 | super() 77 | 78 | this.integer = integer 79 | } 80 | } 81 | ``` 82 | 83 | Now we need to write a handler for our method, it's also very simple, we just need to write a class that extends from `BaseRequestHandler` class and implement two methods: 84 | 85 | ```ts 86 | export class RandomIntHandler extends BaseRequestHandler { 87 | public async handle(request: RandomIntRequest): Promise { 88 | 89 | let randomInt = Math.round(Math.random() * (request.max - request.min) + request.min) 90 | 91 | return new RandomIntResponse(randomInt) 92 | } 93 | 94 | public supports(request: Request): boolean { 95 | return request instanceof RandomIntRequest 96 | } 97 | } 98 | ``` 99 | 100 | handle method gets an instance of our request method and returns an instance of response. supports method just tells the system which requests are supported by this handler. 101 | 102 | Now we just need to set this up: 103 | 104 | ```ts 105 | let logger = new ConsoleLogger() 106 | 107 | const server = new AirshipAPIServer({ 108 | requestsProvider: new HttpRequestsProvider( 109 | logger, 110 | 7000, 111 | 112 | RandomIntRequest 113 | ), 114 | 115 | requestsHandler: new RequestHandlersManager([ 116 | new RandomIntHandler() 117 | ]) 118 | }) 119 | 120 | server.start() 121 | ``` 122 | 123 | So the top level component is `AirshipAPIServer`, you need to pass at least two things to it: requests provider which somehow gets request. In our case, we are using `HttpRequestsProvider`, so requests are coming from the network over HTTP at `7000` port. `HttpRequestsProvider` needs a logger, the port, and list of supported request models. 124 | 125 | The second required argument is `requestsHandler`, because in our case we have just one method - we could have been passed just an instance of `RandomIntHandler`. But when you have several methods you should use `RequestHandlersManager`, that class itself extends `BaseRequestHandler` and finds which handler can handle any request, you just need to pass your handlers. 126 | 127 | The last thing we need to do is start the server, that's it! 128 | 129 | 130 | # Models serialization 131 | 132 | Let's start with example: 133 | 134 | ```ts 135 | class Square { 136 | public width: number 137 | 138 | public height: number 139 | 140 | constructor( 141 | width: number, 142 | height: number 143 | ) { 144 | this.width = width 145 | this.height = height 146 | } 147 | } 148 | ``` 149 | 150 | Here we have simple Square model, first of all if we want this model to be serializable - we need to implement `ISerializable` interface: 151 | 152 | ```ts 153 | class Square implements ISerializable { 154 | public width: number 155 | 156 | public height: number 157 | 158 | constructor( 159 | width: number, 160 | height: number 161 | ) { 162 | this.width = width 163 | this.height = height 164 | } 165 | } 166 | ``` 167 | 168 | Now we need to point our fields using `@serializable` decorator: 169 | 170 | ```ts 171 | class Square implements ISerializable { 172 | @serializable() 173 | public width: number 174 | 175 | @serializable() 176 | public height: number 177 | 178 | constructor( 179 | width: number, 180 | height: number 181 | ) { 182 | this.width = width 183 | this.height = height 184 | } 185 | } 186 | ``` 187 | 188 | `@serializable` decorator is pretty simple: 189 | 190 | ```ts 191 | serializable(name?: string, arrayType?: Function) 192 | ``` 193 | 194 | name - is the name of your property, you can leave it empty & system will automatically detect it, if property name starts with "_" system will replace it with empty string ("_name" -> "name") 195 | 196 | `arrayType` - since typescript cant provide us type of array items you should pass it yourself, for example if you have property ids: number[] - `@serializable` call should look like this: 197 | 198 | ```ts 199 | @serializable('ids', Number) 200 | public ids: number[] 201 | ``` 202 | 203 | Now we can serialize and `deserialize` our model, there is a `BaseSerializer` class which implements common logic and we have `JSONSerializer` which extends from `BaseSerializer`. 204 | 205 | There are two methods that we need: 206 | 207 | ```ts 208 | public static serialize(entity: ISerializable): Object 209 | 210 | public static deserialize( 211 | serializableType: ISerializable & Function, 212 | raw: { [key: string]: any } 213 | ) 214 | ``` 215 | 216 | First one gets your model instance and serializes in, second one `deserializes` it: 217 | 218 | ```ts 219 | JSONSerializer.serialize(new Square(10, 10)) 220 | // { "width": 10, "height": 10 } 221 | ``` 222 | 223 | 224 | ```ts 225 | JSONSerializer.deserialize(Square, { "width": 10, "height": 10 }) 226 | // Square { width: 10, height: 10 } 227 | ``` 228 | 229 | `deserialize` method also does type checking and existing checking. 230 | 231 | 232 | # Logger 233 | 234 | There is a `BaseLogger` interface that specifies basic logging capabilities: 235 | 236 | ```ts 237 | export default interface BaseLogger { 238 | log(prefix: string, data?: any, recursiveDepth?: boolean): void 239 | 240 | warn(prefix: string, data?: any, recursiveDepth?: boolean): void 241 | 242 | error(prefix: string, data?: any, recursiveDepth?: boolean): void 243 | } 244 | ``` 245 | 246 | You can implement your own logger using this interface or you can use `ConsoleLogger` which implements `BaseLogger` and writes logs to `stdout` & `stderr` 247 | 248 | # Handlers 249 | 250 | There are two ways to write handlers at this moment. The first option is to extend `BaseRequestHandler`: 251 | 252 | ```ts 253 | export default class TestHandler extends BaseRequestHandler { 254 | // here you handle request and return response 255 | // it's better to specify concrete types of request & response 256 | public async handle(request: TestRequest): Promise { 257 | return new SuccessResponse() 258 | } 259 | 260 | // here you must return true if you support request 261 | public supports(request: Request): boolean { 262 | return request instanceof TestRequest 263 | } 264 | } 265 | ``` 266 | 267 | Because `BaseRequestHandler` is on great for handling multiply request - there is an subclass of it called `MultiRequestHandler`. 268 | You can use `MultiRequestHandler` like this: 269 | 270 | ```ts 271 | 272 | export default class UsersHandler extends MultiRequestHandler { 273 | // here you pass your request class to handles decorator 274 | @handles(GetUserRequest) 275 | // all GetUserRequest`s will be passed to this method at this moment 276 | public async handleGetUser(request: GetUserRequest): Promise { 277 | 278 | } 279 | 280 | @handles(SaveUserRequest) 281 | public async handleSaveUser(request: SaveUserRequest): Promise { 282 | 283 | } 284 | } 285 | ``` 286 | 287 | All requests are subclasses of `ASRequest` and all responses are subclasses of `ASResponse`. 288 | There are two already implemented responses: 289 | 290 | - ASSuccessResponse 291 | - ASErrorResponse 292 | 293 | # Cache 294 | 295 | There is a `BaseCache` abstract class which specifies basic caching capabilities: 296 | 297 | ```ts 298 | export abstract class BaseCache { 299 | public abstract async cache(key: K, value: V|null, ttl?: number): Promise 300 | 301 | public abstract async get(key: K): Promise 302 | 303 | public abstract async getTTL(key: K): Promise 304 | 305 | public abstract async del(key: K): Promise 306 | 307 | public abstract async setnx(key: K, value: V): Promise 308 | 309 | public abstract async getset(key: K, value: V): Promise 310 | 311 | public abstract async expire(key: K, ttl: number): Promise 312 | 313 | public abstract async keys(key: string): Promise 314 | 315 | public abstract async exists(key: K): Promise 316 | } 317 | ``` 318 | 319 | You can implement your own cache using this interface or you can use `MemoryCache` which implements `BaseCache` and stores data in memory (not all methods are implemented in `MemoryCache`). 320 | `BaseCache` is not used in system, but it's may be useful in your project. 321 | 322 | # API Server 323 | 324 | Main logic for API Server is implemented at `AirshipAPIServer`, implementation is pretty simple: it just wait's for requests from `RequestsProvider`, passes them to `BaseRequestHandler` and returns responses back to `RequestsProvider`. 325 | 326 | To create server you need to pass config object to `AirshipAPIServer` constructor, config object must mach this interface: 327 | 328 | ```ts 329 | export interface AirshipAPIServerConfig { 330 | // Your requests handler 331 | requestsHandler: BaseRequestHandler, 332 | // Your RequestsProvider e.g. HttpRequestsProvider 333 | requestsProvider: RequestsProvider, 334 | // optional statistics counter 335 | statisticsCounter?: BaseStatisticsCounter, 336 | // optional logger 337 | logger?: BaseLogger 338 | } 339 | ``` 340 | 341 | Because `AirshipAPIServer` uses just one request handler it's expected that handler is capable of handling all types of request of your system. 342 | There is a subclass of `BaseRequestHandler` called `RequestHandlerManager` for that purposes. `RequestHandlerManager` receives array of all of your handlers and passes each request to handler that supports it. 343 | 344 | # Requests provider 345 | 346 | Request provider is a component thet provides requests to `AirshipAPIServer`. There is a `RequestsProvider` class: 347 | 348 | ```ts 349 | export abstract class RequestsProvider { 350 | public abstract getRequests( 351 | callback: ( 352 | request: ASRequest, 353 | answerRequest: (response: ASResponse) => void 354 | ) => void 355 | ): void 356 | } 357 | ``` 358 | 359 | `getRequests` method is called by `AirshipAPIServer` to subscribe to requests, response for request is returned by `AirshipAPIServer` to `answerRequest` function. 360 | 361 | At this moment we have requests provider for http: `HttpRequestsProvider`, it uses `dietjs` module for networking. 362 | Usage is pretty simple: 363 | 364 | ```ts 365 | new HttpRequestsProvider( 366 | logger, // instance of BaseLogger 367 | 7000, // port 368 | 369 | // list of your requests 370 | GetUserRequest, 371 | SaveUserRequest 372 | ) 373 | ``` 374 | 375 | Feel free to create more requests providers! 376 | 377 | # Statistics 378 | 379 | If you pass subclass of `BaseStatisticsCounter` to `AirshipAPIServer` server will call `countRequestHit` when request comes and `doneRequest` when request handled. 380 | There is a subclass called `LocalStatisticsCounter` which prints stats using your logger: 381 | 382 | ```ts 383 | let statsCounter = new LocalStatisticsCounter( 384 | logger, 385 | false, // silence flag, handy for debugging 386 | 1000 * 60 // logFrequency in ms, default is 5000 387 | ) 388 | ``` 389 | 390 | # Generating api scheme 391 | 392 | What is api scheme? It's a json file which describes your server models, responses and requests. Api scheme is used to generate client adk & docs. 393 | To generate scheme first you need to create config file which implements `ApiServerConfig` interface: 394 | 395 | ```ts 396 | import {AirshipAPIServerConfig} from "airship-server" 397 | 398 | const config: ApiServerConfig = { 399 | endpoints: [ 400 | [TestRequest, TestResponse], 401 | [GetUserRequest, GetUserResponse] 402 | ] 403 | } 404 | 405 | export default config 406 | ``` 407 | 408 | `endpoints` is just and array of request response pairs. 409 | 410 | After you done with that you can use `aschemegen` tool to generate scheme: 411 | 412 | ```sh 413 | node_modules/.bin/aschemegen --o=/Users/altox/Desktop/test-server/scheme --c=/Users/altox/Desktop/test-server/build/config.js 414 | ``` 415 | 416 | `o` argument is the absolute path where scheme will be saved 417 | `c` argument is the absolute path of compiled config file 418 | 419 | # Generating client SDK 420 | 421 | To generate client SDK you just need to run `asdkgen` and pass to it path to your api schemes and output path: 422 | 423 | ```sh 424 | node_modules/.bin/asdkgen --s=/Users/altox/Desktop/test-server/scheme --o=/Users/altox/Desktop/test-server/sdk 425 | ``` 426 | 427 | SDK is fully statically typed and written in TypeScript, so you can use it in your TS projects and you can compile it and use in your JS projects. 428 | SDK uses `fetch`, so you might need some polyfill. 429 | 430 | # Generating docs 431 | 432 | Generating docs is just like generating SDK, but you want to use `asdocgen`: 433 | 434 | ```sh 435 | node_modules/.bin/asdocgen --s=/Users/altox/Desktop/test-server/scheme --o=/Users/altox/Desktop/test-server/docs 436 | ``` 437 | 438 | 439 | -------------------------------------------------------------------------------- /airship.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Naltox/airship/7185a56848955671c9d414b6a97450f4b4f537ea/airship.png -------------------------------------------------------------------------------- /bin/aschemegen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../build/modules/schemeGenerator/presentation/AirshipSchemeGeneratorApplication.js') -------------------------------------------------------------------------------- /bin/asdkgen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../build/modules/sdkGenerator/presentation/AirshipSDKGeneratorApplication.js') -------------------------------------------------------------------------------- /bin/asdocgen: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../build/modules/doc/presentation/DocsGeneratorApplication.js') -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "airship-server", 3 | "version": "1.0.7", 4 | "description": "Airship is a framework for Node.JS & TypeScript that helps you to write big, scalable and maintainable API servers.", 5 | "main": "build/index.js", 6 | "types": "build/index.d.ts", 7 | "scripts": { 8 | "build": "./node_modules/.bin/tsc --p ./tsconfig.json", 9 | "prepublishOnly": "./node_modules/.bin/tsc --p ./tsconfig.json" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/Naltox/airship" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/Naltox/airship" 17 | }, 18 | "homepage": "https://github.com/Naltox/airship", 19 | "bin": { 20 | "aschemegen": "./bin/aschemegen", 21 | "asdkgen": "./bin/asdkgen", 22 | "asdocgen": "./bin/asdocgen" 23 | }, 24 | "author": "Narek Abovyan ", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "@types/node": "^8.0.47", 28 | "typescript": "^2.7.1", 29 | "typings": "^2.1.0" 30 | }, 31 | "dependencies": { 32 | "diet": "^0.10.8", 33 | "reflect-metadata": "^0.1.9" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * serialize 3 | */ 4 | 5 | export * from './modules/serialize/BaseSerializer' 6 | export {default as JSONSerializer} from './modules/serialize/JSONSerializer' 7 | 8 | /** 9 | * logger 10 | */ 11 | 12 | export {default as BaseLogger} from './modules/logger/domain/BaseLogger' 13 | export {default as ConsoleLogger} from './modules/logger/infrustructure/ConsoleLogger' 14 | 15 | 16 | /** 17 | * apiServer 18 | */ 19 | export {default as AirshipAPIServer, AirshipAPIServerConfig} from './modules/apiServer/application/AirshipAPIServer' 20 | export {default as ASErrorResponse} from './modules/apiServer/domain/entity/ASErrorResponse' 21 | export * from './modules/apiServer/domain/entity/ASRequest' 22 | export * from './modules/apiServer/domain/entity/ASResponse' 23 | export {default as ASSuccessResponse} from './modules/apiServer/domain/entity/ASSuccessResponse' 24 | export * from './modules/apiServer/domain/BaseRequestHandler' 25 | export * from './modules/apiServer/domain/RequestsProvider' 26 | export * from './modules/apiServer/domain/ServerConfig' 27 | export {default as HttpRequestsProvider} from './modules/apiServer/infrastructure/HttpRequestsProvider' 28 | export * from './modules/apiServer/infrastructure/MultyRequestHandler' 29 | export {default as RequestHandlersManager} from './modules/apiServer/infrastructure/RequestHandlersManager' 30 | 31 | /** 32 | * cache 33 | */ 34 | 35 | export * from './modules/cache/domain/BaseCache' 36 | export {default as MemoryCache} from './modules/cache/infrustructure/MemoryCache' 37 | 38 | /** 39 | * statistics 40 | */ 41 | 42 | export * from './modules/statistics/domain/BaseStatisticsCounter' 43 | export {default as LocalStatisticsCounter} from './modules/statistics/infrastructure/LocalStatisticsCounter' 44 | 45 | /** 46 | * codeGen 47 | */ 48 | 49 | export {default as ApiMethodParam} from './modules/codeGen/domain/schema/ApiMethodParam' 50 | export {default as ApiMethodScheme} from './modules/codeGen/domain/schema/ApiMethodScheme' 51 | export {default as ClassField} from './modules/codeGen/domain/schema/ClassField' 52 | export {default as ClassScheme} from './modules/codeGen/domain/schema/ClassScheme' 53 | export {default as AnyType} from './modules/codeGen/domain/types/AnyType' 54 | export {default as BooleanType} from './modules/codeGen/domain/types/BooleanType' 55 | export {default as CustomType} from './modules/codeGen/domain/types/CustomType' 56 | export {default as IntBoolType} from './modules/codeGen/domain/types/IntBoolType' 57 | export {default as NumberType} from './modules/codeGen/domain/types/NumberType' 58 | export {default as ObjectType} from './modules/codeGen/domain/types/ObjectType' 59 | export {default as StringType} from './modules/codeGen/domain/types/StringType' 60 | export * from './modules/codeGen/domain/types/Type' 61 | export {default as VectorType} from './modules/codeGen/domain/types/VectorType' 62 | export * from './modules/codeGen/domain/CodeGenerator' 63 | export {default as CodeLine} from './modules/codeGen/domain/CodeLine' 64 | export {default as SourceCode} from './modules/codeGen/domain/SourceCode' 65 | export {default as JavaScriptCodeGenerator} from './modules/codeGen/infrastructure/JavaScriptCodeGenerator' 66 | export {default as SwiftCodeGenerator} from './modules/codeGen/infrastructure/SwiftCodeGenerator' 67 | export {default as TypescriptCodeGenerator} from './modules/codeGen/infrastructure/TypescriptCodeGenerator' 68 | export * from './modules/codeGen/infrastructure/Utils' 69 | 70 | /** 71 | * schemeGenerator 72 | */ 73 | 74 | export {default as ApiSchema} from './modules/schemeGenerator/domain/ApiSchema' 75 | export * from './modules/schemeGenerator/domain/ApiSchemeGenerator' 76 | export {default as AirshipApiSchemeGenerator} from './modules/schemeGenerator/infrastructure/AirshipApiSchemeGenerator' 77 | 78 | /** 79 | * sdkGenerator 80 | */ 81 | 82 | export * from './modules/sdkGenerator/domain/ApiSDKGenerator' 83 | export * from './modules/sdkGenerator/domain/SDKConfig' 84 | export {default as SDKFile} from './modules/sdkGenerator/domain/SDKFile' 85 | export {default as AirshipApiSDKGenerator} from './modules/sdkGenerator/infrastructure/AirshipApiSDKGenerator' -------------------------------------------------------------------------------- /src/modules/apiServer/application/AirshipAPIServer.ts: -------------------------------------------------------------------------------- 1 | import {BaseRequestHandler} from "../domain/BaseRequestHandler"; 2 | import {RequestsProvider} from "../domain/RequestsProvider"; 3 | import BaseLogger from "../../logger/domain/BaseLogger"; 4 | import {ASRequest} from "../domain/entity/ASRequest"; 5 | import {ASResponse} from "../domain/entity/ASResponse"; 6 | import ErrorResponse from "../domain/entity/ASErrorResponse"; 7 | import {BaseStatisticsCounter} from "../../statistics/domain/BaseStatisticsCounter"; 8 | 9 | export interface AirshipAPIServerConfig { 10 | requestsHandler: BaseRequestHandler, 11 | requestsProvider: RequestsProvider, 12 | statisticsCounter?: BaseStatisticsCounter, 13 | logger?: BaseLogger 14 | } 15 | 16 | /** 17 | * AirshipAPIServer is the main API server 18 | */ 19 | export default class AirshipAPIServer { 20 | constructor( 21 | private _config: AirshipAPIServerConfig 22 | ) { 23 | 24 | } 25 | 26 | private log(prefix: string, data?: any, depth?: boolean) { 27 | if (this._config.logger) 28 | this._config.logger.log(prefix, data, depth) 29 | } 30 | 31 | public async start() { 32 | this.log('Welcome to Airship API server') 33 | this._config.requestsProvider.getRequests((request, answerRequest) => { 34 | this.handleRequest(request, answerRequest) 35 | }) 36 | } 37 | 38 | public async handleRequest(request: ASRequest, answerRequest: (response: ASResponse) => void) { 39 | this.log('Got request', request) 40 | 41 | if (this._config.statisticsCounter) 42 | this._config.statisticsCounter.countRequestHit() 43 | 44 | try { 45 | let response = await this._config.requestsHandler.handle(request) 46 | 47 | this.log('Got response for request', { 48 | request, 49 | response: response 50 | }, true) 51 | 52 | answerRequest(response) 53 | 54 | if (this._config.statisticsCounter) 55 | this._config.statisticsCounter.doneRequest() 56 | } 57 | catch (e) { 58 | console.error(e) 59 | 60 | if (e instanceof ErrorResponse) { 61 | answerRequest(e) 62 | return 63 | } 64 | 65 | answerRequest( 66 | new ErrorResponse( 67 | e.message || 'Unknown error', 68 | e.code || 0 69 | ) 70 | ) 71 | 72 | if (this._config.statisticsCounter) 73 | this._config.statisticsCounter.doneRequest() 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /src/modules/apiServer/domain/BaseRequestHandler.ts: -------------------------------------------------------------------------------- 1 | import {ASResponse} from './entity/ASResponse' 2 | import {ASRequest} from './entity/ASRequest' 3 | 4 | export abstract class BaseRequestHandler { 5 | public abstract async handle(request: ASRequest): Promise 6 | 7 | public abstract supports(request: ASRequest): boolean 8 | } -------------------------------------------------------------------------------- /src/modules/apiServer/domain/RequestsProvider.ts: -------------------------------------------------------------------------------- 1 | import {ASRequest} from './entity/ASRequest' 2 | import {ASResponse} from './entity/ASResponse' 3 | 4 | export abstract class RequestsProvider { 5 | public abstract getRequests( 6 | callback: ( 7 | request: ASRequest, 8 | answerRequest: (response: ASResponse) => void 9 | ) => void 10 | ): void 11 | } -------------------------------------------------------------------------------- /src/modules/apiServer/domain/ServerConfig.ts: -------------------------------------------------------------------------------- 1 | import {ASRequest} from "./entity/ASRequest"; 2 | import {ASResponse} from "./entity/ASResponse"; 3 | 4 | export interface ApiServerConfig { 5 | endpoints: [[ASRequest, ASResponse]] 6 | } -------------------------------------------------------------------------------- /src/modules/apiServer/domain/entity/ASErrorResponse.ts: -------------------------------------------------------------------------------- 1 | import {ASResponse} from "./ASResponse"; 2 | import {ISerializable, serializable} from "../../../serialize/BaseSerializer"; 3 | 4 | export default class ASErrorResponse extends ASResponse implements ISerializable { 5 | @serializable() 6 | private _ok: boolean 7 | 8 | @serializable() 9 | private _error: string 10 | 11 | @serializable() 12 | private _errorCode: number 13 | 14 | constructor(error: string, errorCode: number = 0) { 15 | super() 16 | this._ok = false 17 | this._error = error 18 | this._errorCode = errorCode 19 | } 20 | 21 | public serialize() { 22 | return { 23 | ok: false, 24 | error: this._error, 25 | errorCode: this._errorCode 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/modules/apiServer/domain/entity/ASRequest.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata" 2 | import { Wrapper } from "../../../codeGen/infrastructure/Utils"; 3 | 4 | export function queryPath(path: string) { 5 | return (target: any) => { 6 | let constructor = target 7 | 8 | if (!constructor.queryPath) 9 | constructor.queryPath = {} 10 | if (!constructor.queryPath[constructor.name]) 11 | constructor.queryPath[constructor.name] = path 12 | } 13 | } 14 | 15 | export type ASRequestType = Wrapper 16 | 17 | export class ASRequest { 18 | public static queryPath: string 19 | 20 | public static getQueryPath(): string { 21 | return (this.queryPath as any)[this.prototype.constructor.name] 22 | } 23 | } -------------------------------------------------------------------------------- /src/modules/apiServer/domain/entity/ASResponse.ts: -------------------------------------------------------------------------------- 1 | import {ISerializable} from "../../../serialize/BaseSerializer"; 2 | 3 | export abstract class ASResponse implements ISerializable { 4 | 5 | } -------------------------------------------------------------------------------- /src/modules/apiServer/domain/entity/ASSuccessResponse.ts: -------------------------------------------------------------------------------- 1 | import {ASResponse} from "./ASResponse"; 2 | import {ISerializable, serializable} from "../../../serialize/BaseSerializer"; 3 | 4 | export default class ASSuccessResponse extends ASResponse implements ISerializable { 5 | @serializable() 6 | private _ok: boolean 7 | 8 | constructor() { 9 | super() 10 | this._ok = true 11 | } 12 | } -------------------------------------------------------------------------------- /src/modules/apiServer/infrastructure/HttpRequestsProvider.ts: -------------------------------------------------------------------------------- 1 | import {RequestsProvider} from "../domain/RequestsProvider"; 2 | import {ASRequest, ASRequestType} from "../domain/entity/ASRequest"; 3 | import {ASResponse} from "../domain/entity/ASResponse"; 4 | import ErrorResponse from "../domain/entity/ASErrorResponse"; 5 | import BaseLogger from "../../logger/domain/BaseLogger"; 6 | import JSONSerializer from "../../serialize/JSONSerializer"; 7 | const Diet = require('diet') 8 | 9 | export default class HttpRequestsProvider extends RequestsProvider { 10 | private _app: any 11 | private _supportedRequests: ASRequestType[] 12 | private _logger: BaseLogger 13 | 14 | private _requestsCallback: (( 15 | request: ASRequest, 16 | answerRequest: (response: ASResponse) => void 17 | ) => void) | null 18 | 19 | constructor( 20 | logger: BaseLogger, 21 | port: number, 22 | ...supportedRequests: ASRequestType[] 23 | ) { 24 | super() 25 | this._logger = logger 26 | this._supportedRequests = supportedRequests 27 | this._requestsCallback = null 28 | 29 | const app = Diet({ silent: true }) 30 | this._app = app 31 | 32 | app.listen(`127.0.0.1:${port}`) 33 | 34 | this._logger.log('HttpRequestsProvider', `Started listening at 127.0.0.1:${port}`) 35 | this._logger.log( 36 | 'HttpRequestsProvider supported requests:\n', 37 | supportedRequests 38 | .map(r => (r as any).prototype.constructor.name) 39 | .join('\n') 40 | ) 41 | 42 | this._supportedRequests.forEach(request => { 43 | let queryPath = (request as any).getQueryPath() 44 | 45 | this.setupMethod( 46 | request, 47 | 'post', 48 | queryPath 49 | ) 50 | }) 51 | } 52 | 53 | public getRequests( 54 | callback: ( 55 | request: ASRequest, 56 | answerRequest: (response: ASResponse) => void 57 | ) => void 58 | ) { 59 | this._requestsCallback = callback 60 | } 61 | 62 | private setupMethod( 63 | request: any, 64 | type: string, 65 | path: string, 66 | ) { 67 | this._app[type](path, ($: any) => { 68 | if (!this._requestsCallback) 69 | throw new Error('No request callback') 70 | 71 | try { 72 | let data = type == 'post' ? $.body : $.query 73 | 74 | if (!data) 75 | $.end(JSON.stringify(JSONSerializer.serialize(new ErrorResponse('No data passed')))) 76 | 77 | let deserializedRequest: ASRequest = JSONSerializer.deserialize( 78 | request, 79 | type == 'post' ? $.body : $.query 80 | ) 81 | 82 | 83 | this._requestsCallback( 84 | deserializedRequest, 85 | (response: ASResponse) => { 86 | $.end(JSON.stringify(JSONSerializer.serialize(response))) 87 | } 88 | ) 89 | } 90 | catch (e) { 91 | $.end(JSON.stringify(JSONSerializer.serialize(new ErrorResponse(e.message)))) 92 | } 93 | }) 94 | } 95 | } -------------------------------------------------------------------------------- /src/modules/apiServer/infrastructure/MultyRequestHandler.ts: -------------------------------------------------------------------------------- 1 | import {BaseRequestHandler} from "../domain/BaseRequestHandler"; 2 | import {ASResponse} from "../domain/entity/ASResponse"; 3 | import {ASRequest, ASRequestType} from "../domain/entity/ASRequest"; 4 | import { Wrapper } from "../../codeGen/infrastructure/Utils"; 5 | 6 | export type HandlerFunction = (request: ASRequest) => ASResponse 7 | 8 | export type MultiRequestHandlerType = Wrapper 9 | 10 | export abstract class MultiRequestHandler extends BaseRequestHandler { 11 | 12 | private static handlers: Map> = new Map() 13 | 14 | public async handle(request: ASRequest): Promise { 15 | let classHandlers = MultiRequestHandler.handlers.get(this.constructor) 16 | 17 | if (!classHandlers || !classHandlers.has(request.constructor as any)) 18 | throw new Error('Cant find request for request: ' + request.constructor.name) 19 | 20 | return classHandlers.get(request.constructor as any)!(request) 21 | } 22 | 23 | public supports(request: ASRequest): boolean { 24 | return ( 25 | MultiRequestHandler.handlers.has(this.constructor) && 26 | MultiRequestHandler.handlers.get(this.constructor)!.has(request.constructor as any) 27 | ) 28 | } 29 | 30 | public addHandler(request: ASRequestType, handlerClass: () => MultiRequestHandlerType, handlerFunction: HandlerFunction) { 31 | if (!MultiRequestHandler.handlers.has(handlerClass)) 32 | MultiRequestHandler.handlers.set(handlerClass, new Map()) 33 | 34 | MultiRequestHandler.handlers.get(handlerClass)!.set(request, handlerFunction) 35 | } 36 | } 37 | 38 | export function handles(request: ASRequestType) { 39 | return (target: T, propertyKey: string) => { 40 | let handler = (target as any)[propertyKey] as HandlerFunction 41 | 42 | target.addHandler(request, target.constructor as any, handler) 43 | } 44 | } -------------------------------------------------------------------------------- /src/modules/apiServer/infrastructure/RequestHandlersManager.ts: -------------------------------------------------------------------------------- 1 | import {BaseRequestHandler} from '../domain/BaseRequestHandler' 2 | import {ASRequest} from '../domain/entity/ASRequest' 3 | import {ASResponse} from '../domain/entity/ASResponse' 4 | 5 | /** 6 | * RequestHandlersManager pretends BaseRequestHandler 7 | * and sends requests to handler that supports it 8 | */ 9 | export default class RequestHandlersManager extends BaseRequestHandler { 10 | constructor( 11 | private _handlers: BaseRequestHandler[] 12 | ) { 13 | super() 14 | } 15 | 16 | public supports(request: ASRequest): boolean { 17 | return true 18 | } 19 | 20 | public handle(request: ASRequest): Promise { 21 | for (const handler of this._handlers) { 22 | if (handler.supports(request)) 23 | return handler.handle(request) 24 | } 25 | 26 | throw new Error('Cant find handler for request') 27 | } 28 | } -------------------------------------------------------------------------------- /src/modules/cache/domain/BaseCache.ts: -------------------------------------------------------------------------------- 1 | export abstract class BaseCache { 2 | /** 3 | * @param ttl TTL in ms 4 | */ 5 | public abstract async cache(key: K, value: V|null, ttl?: number): Promise 6 | 7 | public abstract async get(key: K): Promise 8 | 9 | /** 10 | * returns TTL in ms 11 | */ 12 | public abstract async getTTL(key: K): Promise 13 | 14 | public abstract async del(key: K): Promise 15 | 16 | public abstract async setnx(key: K, value: V): Promise 17 | 18 | public abstract async getset(key: K, value: V): Promise 19 | 20 | public abstract async expire(key: K, ttl: number): Promise 21 | 22 | public abstract async keys(key: string): Promise 23 | 24 | public abstract async exists(key: K): Promise 25 | } -------------------------------------------------------------------------------- /src/modules/cache/infrustructure/MemoryCache.ts: -------------------------------------------------------------------------------- 1 | import {BaseCache} from "../domain/BaseCache"; 2 | 3 | export default class MemoryCache extends BaseCache { 4 | private _storage: Map 5 | 6 | private _ttls: Map 7 | 8 | constructor() { 9 | super() 10 | this._storage = new Map() 11 | this._ttls = new Map() 12 | } 13 | 14 | public async cache(key: K, value: V, ttl?: number) { 15 | this._storage.set(key, value) 16 | 17 | if (!ttl) 18 | return 19 | 20 | this._ttls.set(key, { 21 | started: Date.now(), 22 | ttl 23 | }) 24 | 25 | setTimeout(() => { 26 | this._ttls.delete(key) 27 | this._storage.delete(key) 28 | }, ttl ) 29 | } 30 | 31 | public async get(key: K): Promise { 32 | return this._storage.get(key) 33 | } 34 | 35 | public async getTTL(key: K): Promise { 36 | let ttlInfo = this._ttls.get(key) 37 | 38 | if (!ttlInfo) 39 | return -1 40 | 41 | let delta = Date.now() - ttlInfo.started 42 | 43 | if (delta > ttlInfo.ttl) 44 | return 0 45 | else 46 | return ttlInfo.ttl - delta 47 | } 48 | 49 | public async del(key: K): Promise { 50 | throw new Error('not implemented') 51 | } 52 | 53 | public async setnx(key: K, value: V): Promise { 54 | throw new Error('not implemented') 55 | } 56 | 57 | public async getset(key: K, value: V): Promise { 58 | throw new Error('not implemented') 59 | } 60 | 61 | public async expire(key: K, ttl: number): Promise { 62 | throw new Error('not implemented') 63 | } 64 | 65 | public async keys(key: string): Promise { 66 | throw new Error('not implemented') 67 | } 68 | 69 | public async exists(key: K): Promise { 70 | throw new Error('not implemented') 71 | } 72 | } -------------------------------------------------------------------------------- /src/modules/codeGen/domain/CodeGenerator.ts: -------------------------------------------------------------------------------- 1 | import ClassScheme from "./schema/ClassScheme"; 2 | import SourceCode from "./SourceCode"; 3 | import ApiMethodScheme from "./schema/ApiMethodScheme"; 4 | 5 | export interface CodeGenerator { 6 | generateClass(scheme: ClassScheme): SourceCode 7 | 8 | generateApiMethod(scheme: ApiMethodScheme): SourceCode 9 | 10 | generateApiMethodParamsInterface(scheme: ApiMethodScheme): SourceCode 11 | } -------------------------------------------------------------------------------- /src/modules/codeGen/domain/CodeLine.ts: -------------------------------------------------------------------------------- 1 | export default class CodeLine { 2 | private _data: string 3 | 4 | constructor(data: string, tab: number = 0) { 5 | this._data = this.genTab(tab) + data 6 | } 7 | 8 | get data(): string { 9 | return this._data 10 | } 11 | 12 | public tab(n: number) { 13 | this._data = this.genTab(n) + this._data 14 | } 15 | 16 | private genTab(n: number): string { 17 | return new Array(n).fill(' ').join('') 18 | } 19 | } -------------------------------------------------------------------------------- /src/modules/codeGen/domain/SourceCode.ts: -------------------------------------------------------------------------------- 1 | import CodeLine from "./CodeLine"; 2 | 3 | export default class SourceCode { 4 | private _lines: CodeLine[] 5 | 6 | constructor(lines: CodeLine[] = []) { 7 | this._lines = lines 8 | } 9 | 10 | public add(data: string, tab: number = 0): SourceCode { 11 | this._lines.push(new CodeLine(data, tab)) 12 | return this 13 | } 14 | 15 | public append(code: SourceCode, tab: number = 0) { 16 | code.tab(tab) 17 | this._lines.push(...code.lines) 18 | } 19 | 20 | public render(): string { 21 | return this._lines.map(line => line.data).join('\n') 22 | } 23 | 24 | public tab(n: number) { 25 | this._lines.forEach(line => line.tab(n)) 26 | } 27 | 28 | get lines(): CodeLine[] { 29 | return this._lines 30 | } 31 | 32 | private genTab(n: number): string { 33 | return new Array(n).join(' ') 34 | } 35 | } -------------------------------------------------------------------------------- /src/modules/codeGen/domain/schema/ApiMethodParam.ts: -------------------------------------------------------------------------------- 1 | import {Type} from "../types/Type"; 2 | 3 | export default class ApiMethodParam { 4 | constructor( 5 | readonly name: string, 6 | readonly type: Type, 7 | readonly required: boolean, 8 | readonly description: string 9 | ) { 10 | 11 | } 12 | 13 | public serialize(): Object { 14 | return { 15 | name: this.name, 16 | type: this.type.serialize(), 17 | required: this.required, 18 | description: this.description 19 | } 20 | } 21 | 22 | public static deserialize(raw: any): ApiMethodParam { 23 | return new ApiMethodParam( 24 | raw['name'], 25 | Type.deserialize(raw['type']), 26 | raw['required'], 27 | raw['description'] 28 | ) 29 | } 30 | } -------------------------------------------------------------------------------- /src/modules/codeGen/domain/schema/ApiMethodScheme.ts: -------------------------------------------------------------------------------- 1 | import ApiMethodParam from "./ApiMethodParam"; 2 | import {Type} from "../types/Type"; 3 | 4 | export default class ApiMethodScheme { 5 | constructor( 6 | readonly name: string, 7 | readonly params: ApiMethodParam[], 8 | readonly responseType: Type, 9 | readonly description: string 10 | ) { 11 | 12 | } 13 | 14 | public serialize(): Object { 15 | return { 16 | name: this.name, 17 | params: this.params.map(p => p.serialize()), 18 | responseType: this.responseType.serialize(), 19 | description: this.description 20 | } 21 | } 22 | 23 | public static deserialize(raw: any): ApiMethodScheme { 24 | return new ApiMethodScheme( 25 | raw['name'], 26 | raw['params'].map(ApiMethodParam.deserialize), 27 | Type.deserialize(raw['responseType']), 28 | raw['description'] 29 | ) 30 | } 31 | } -------------------------------------------------------------------------------- /src/modules/codeGen/domain/schema/ClassField.ts: -------------------------------------------------------------------------------- 1 | import {Type} from "../types/Type"; 2 | 3 | export default class ClassField { 4 | constructor( 5 | readonly name: string, 6 | readonly type: Type, 7 | readonly description: string 8 | ) { 9 | 10 | } 11 | 12 | public serialize(): Object { 13 | return { 14 | name: this.name, 15 | type: this.type.serialize(), 16 | description: this.description 17 | } 18 | } 19 | 20 | public static deserialize(raw: any): ClassField { 21 | return new ClassField( 22 | raw['name'], 23 | Type.deserialize(raw['type']), 24 | raw['description'] 25 | ) 26 | } 27 | } -------------------------------------------------------------------------------- /src/modules/codeGen/domain/schema/ClassScheme.ts: -------------------------------------------------------------------------------- 1 | import ClassField from "./ClassField"; 2 | 3 | export default class ClassScheme { 4 | constructor( 5 | readonly name: string, 6 | readonly fields: ClassField[] 7 | ) { 8 | 9 | } 10 | 11 | public serialize(): Object { 12 | return { 13 | name: this.name, 14 | fields: this.fields.map(f => f.serialize()) 15 | } 16 | } 17 | 18 | public static deserialize(raw: any): ClassScheme { 19 | return new ClassScheme( 20 | raw['name'], 21 | raw['fields'].map(ClassField.deserialize) 22 | ) 23 | } 24 | } -------------------------------------------------------------------------------- /src/modules/codeGen/domain/types/AnyType.ts: -------------------------------------------------------------------------------- 1 | import {Type} from "./Type"; 2 | 3 | export default class AnyType implements Type { 4 | public serialize(): Object { 5 | return { 6 | type: 'AnyType' 7 | } 8 | } 9 | 10 | public static deserialize(raw: Object): AnyType { 11 | return new AnyType() 12 | } 13 | } -------------------------------------------------------------------------------- /src/modules/codeGen/domain/types/BooleanType.ts: -------------------------------------------------------------------------------- 1 | import {Type} from "./Type"; 2 | 3 | export default class BooleanType implements Type { 4 | public serialize(): Object { 5 | return { 6 | type: 'BooleanType' 7 | } 8 | } 9 | 10 | public static deserialize(raw: Object): BooleanType { 11 | return new BooleanType() 12 | } 13 | } -------------------------------------------------------------------------------- /src/modules/codeGen/domain/types/CustomType.ts: -------------------------------------------------------------------------------- 1 | import {Type} from "./Type"; 2 | 3 | export default class CustomType implements Type { 4 | constructor(readonly name: string) { 5 | 6 | } 7 | 8 | public serialize(): Object { 9 | return { 10 | type: 'CustomType', 11 | name: this.name 12 | } 13 | } 14 | 15 | public static deserialize(raw: any): CustomType { 16 | return new CustomType(raw['name']) 17 | } 18 | } -------------------------------------------------------------------------------- /src/modules/codeGen/domain/types/IntBoolType.ts: -------------------------------------------------------------------------------- 1 | import {Type} from "./Type"; 2 | 3 | export default class IntBoolType implements Type { 4 | serialize(): Object { 5 | return {} 6 | } 7 | } -------------------------------------------------------------------------------- /src/modules/codeGen/domain/types/NumberType.ts: -------------------------------------------------------------------------------- 1 | import {Type} from "./Type"; 2 | 3 | export default class NumberType implements Type { 4 | public serialize(): Object { 5 | return { 6 | type: 'NumberType' 7 | } 8 | } 9 | 10 | public static deserialize(raw: Object): NumberType { 11 | return new NumberType() 12 | } 13 | } -------------------------------------------------------------------------------- /src/modules/codeGen/domain/types/ObjectType.ts: -------------------------------------------------------------------------------- 1 | import {Type} from "./Type"; 2 | 3 | export default class ObjectType implements Type { 4 | public serialize(): Object { 5 | return { 6 | type: 'ObjectType' 7 | } 8 | } 9 | 10 | public static deserialize(raw: Object): ObjectType { 11 | return new ObjectType() 12 | } 13 | } -------------------------------------------------------------------------------- /src/modules/codeGen/domain/types/StringType.ts: -------------------------------------------------------------------------------- 1 | import {Type} from "./Type"; 2 | 3 | export default class StringType implements Type { 4 | public serialize(): Object { 5 | return { 6 | type: 'StringType' 7 | } 8 | } 9 | 10 | public static deserialize(raw: Object): StringType { 11 | return new StringType() 12 | } 13 | } -------------------------------------------------------------------------------- /src/modules/codeGen/domain/types/Type.ts: -------------------------------------------------------------------------------- 1 | import AnyType from "./AnyType"; 2 | import BooleanType from "./BooleanType"; 3 | import CustomType from "./CustomType"; 4 | import NumberType from "./NumberType"; 5 | import StringType from "./StringType"; 6 | import VectorType from "./VectorType"; 7 | import ObjectType from "./ObjectType"; 8 | 9 | export abstract class Type { 10 | public abstract serialize(): Object 11 | 12 | public static deserialize(raw: any): Type { 13 | switch (raw['type']) { 14 | case 'AnyType': 15 | return AnyType.deserialize(raw) 16 | case 'BooleanType': 17 | return BooleanType.deserialize(raw) 18 | case 'CustomType': 19 | return CustomType.deserialize(raw) 20 | case 'NumberType': 21 | return NumberType.deserialize(raw) 22 | case 'StringType': 23 | return StringType.deserialize(raw) 24 | case 'VectorType': 25 | return VectorType.deserialize(raw) 26 | case 'ObjectType': 27 | return ObjectType.deserialize(raw) 28 | } 29 | 30 | throw 'UNKNOWN TYPE' 31 | } 32 | } -------------------------------------------------------------------------------- /src/modules/codeGen/domain/types/VectorType.ts: -------------------------------------------------------------------------------- 1 | import {Type} from "./Type"; 2 | 3 | export default class VectorType implements Type { 4 | constructor(readonly item: Type) { 5 | 6 | } 7 | 8 | public serialize(): Object { 9 | return { 10 | type: 'VectorType', 11 | item: this.item.serialize() 12 | } 13 | } 14 | 15 | public static deserialize(raw: any): VectorType { 16 | return new VectorType(Type.deserialize(raw['item'])) 17 | } 18 | } -------------------------------------------------------------------------------- /src/modules/codeGen/infrastructure/JavaScriptCodeGenerator.ts: -------------------------------------------------------------------------------- 1 | import {CodeGenerator} from "../domain/CodeGenerator"; 2 | import ClassScheme from "../domain/schema/ClassScheme"; 3 | import SourceCode from "../domain/SourceCode"; 4 | import {Type} from "../domain/types/Type"; 5 | import CustomType from "../domain/types/CustomType"; 6 | import {toCamelCase} from "./Utils"; 7 | import VectorType from "../domain/types/VectorType"; 8 | import IntBoolType from "../domain/types/IntBoolType"; 9 | import StringType from "../domain/types/StringType"; 10 | import NumberType from "../domain/types/NumberType"; 11 | import AnyType from "../domain/types/AnyType"; 12 | import BooleanType from "../domain/types/BooleanType"; 13 | import ApiMethodScheme from "../domain/schema/ApiMethodScheme"; 14 | 15 | export default class JavaScriptCodeGenerator implements CodeGenerator { 16 | public generateClass(scheme: ClassScheme): SourceCode { 17 | let code = new SourceCode() 18 | let imports = this.generateImports(scheme) 19 | let constructor = this.generateClassConstructor(scheme) 20 | let deserializeMethod = this.generateDeserializeMethod(scheme) 21 | let serializeMethod = this.generateSerializeMethod(scheme) 22 | 23 | code.append(imports) 24 | code.add('') 25 | code.add(`class ${scheme.name} {`) 26 | code.append(constructor, 1) 27 | code.add('') 28 | code.append(deserializeMethod, 1) 29 | code.add('') 30 | code.append(serializeMethod, 1) 31 | code.add('}') 32 | code.add('') 33 | code.add(`module.exports = ${scheme.name}`) 34 | 35 | return code 36 | } 37 | 38 | public generateApiMethod(scheme: ApiMethodScheme): SourceCode { 39 | throw new Error('Not implemented') 40 | } 41 | 42 | public generateApiMethodParamsInterface(scheme: ApiMethodScheme): SourceCode { 43 | throw new Error('Not implemented') 44 | } 45 | 46 | private generateImports(scheme: ClassScheme): SourceCode { 47 | let code = new SourceCode() 48 | 49 | scheme.fields.forEach((field, index) => { 50 | let customType = this.getCustomType(field.type) 51 | 52 | if (customType) { 53 | code.add(`const ${customType.name} = require('./${customType.name}')`) 54 | } 55 | }) 56 | 57 | return code 58 | } 59 | 60 | private generateClassConstructor(scheme: ClassScheme): SourceCode { 61 | let code = new SourceCode() 62 | let jsdoc = this.generateClassConstructorJSDoc(scheme) 63 | 64 | code.append(jsdoc) 65 | 66 | code.add('constructor (') 67 | 68 | scheme.fields.forEach((field, index) => { 69 | let coma = this.genComa(scheme.fields, index) 70 | 71 | code.add(`${toCamelCase(field.name)}${coma}`, 1) 72 | }) 73 | 74 | code.add(') {') 75 | scheme.fields.forEach((field, index) => { 76 | code.add(`this.${toCamelCase(field.name)} = ${toCamelCase(field.name)}`, 1) 77 | }) 78 | code.add('}') 79 | 80 | return code 81 | } 82 | 83 | private generateClassConstructorJSDoc(scheme: ClassScheme): SourceCode { 84 | let code = new SourceCode() 85 | 86 | code.add('/**') 87 | code.add(' * @class') 88 | 89 | scheme.fields.forEach(field => { 90 | code.add(` * @property {${this.renderType(field.type)}} ${toCamelCase(field.name)} ${field.description}`) 91 | }) 92 | 93 | code.add(' */') 94 | 95 | return code 96 | } 97 | 98 | private generateDeserializeMethod(scheme: ClassScheme): SourceCode { 99 | let code = new SourceCode() 100 | 101 | code.add('/**') 102 | code.add(' * @param {Object} raw') 103 | code.add(` * @returns {${scheme.name}}`) 104 | code.add(' */') 105 | 106 | code.add(`static deserialize(raw) {`) 107 | code.add(`return new ${scheme.name} (`, 1) 108 | 109 | scheme.fields.forEach((field, index) => { 110 | let coma = this.genComa(scheme.fields, index) 111 | let fieldVar = `raw['${field.name}']` 112 | 113 | if (field.type instanceof VectorType) 114 | code.add(this.renderVectorDeserialize(fieldVar, field.type) + coma, 2) 115 | else if (field.type instanceof CustomType) 116 | code.add(`${fieldVar} ? ${field.type.name}.deserialize(${fieldVar}) : undefined${coma}`, 2) 117 | else if (field.type instanceof IntBoolType) 118 | code.add(`!!${fieldVar}${coma}`, 2) 119 | else 120 | code.add(fieldVar + coma, 2) 121 | }) 122 | 123 | code.add(`)`, 1) 124 | code.add('}') 125 | 126 | return code 127 | } 128 | 129 | private generateSerializeMethod(scheme: ClassScheme): SourceCode { 130 | let code = new SourceCode() 131 | 132 | code.add('/**') 133 | code.add(` * @returns {Object}`) 134 | code.add(' */') 135 | 136 | code.add(`serialize() {`) 137 | 138 | code.add(`return {`, 1) 139 | 140 | 141 | scheme.fields.forEach((field, index) => { 142 | let coma = this.genComa(scheme.fields, index) 143 | let fieldVar = `${field.name}: this.${toCamelCase(field.name)}` 144 | 145 | if (field.type instanceof VectorType) 146 | code.add(`${field.name}: ${this.renderVectorSerialize(`this.${toCamelCase(field.name)}`, field.type) + coma}`, 2) 147 | else if (field.type instanceof CustomType) 148 | code.add(`${fieldVar} ? this.${toCamelCase(field.name)}.serialize() : undefined${coma}`, 2) 149 | else 150 | code.add(fieldVar + coma, 2) 151 | }) 152 | 153 | 154 | code.add('}', 1) 155 | code.add('}') 156 | 157 | return code 158 | } 159 | 160 | private renderType(type: Type, withoutUndefined = false): string { 161 | if (type instanceof StringType) 162 | return 'string' 163 | 164 | if (type instanceof NumberType) 165 | return 'number' 166 | 167 | if (type instanceof AnyType) 168 | return 'any' 169 | 170 | if (type instanceof BooleanType) 171 | return 'boolean' 172 | 173 | if (type instanceof IntBoolType) 174 | return 'boolean' 175 | 176 | if (type instanceof CustomType) 177 | return type.name + `${!withoutUndefined ? '|undefined' : ''}` 178 | 179 | if (type instanceof VectorType) { 180 | return this.renderType(type.item, true) + `[]${!withoutUndefined ? '|undefined' : ''}` 181 | } 182 | 183 | throw new Error('UNSUPPORTED TYPE' + JSON.stringify(type)) 184 | } 185 | 186 | private genComa(list: any[], index: number): string { 187 | return (index == list.length - 1) ? '' : ',' 188 | } 189 | 190 | private renderVectorDeserialize(value: string, type: Type): string { 191 | let code = '' 192 | 193 | if (type instanceof VectorType) 194 | code += `${value} ? ${value}.map(v => ${this.renderVectorDeserialize('v', type.item)}) : undefined` 195 | else if (type instanceof CustomType) 196 | code += `${value} ? ${type.name}.deserialize(${value}) : undefined` 197 | else 198 | code += value 199 | 200 | return code 201 | } 202 | 203 | private renderVectorSerialize(value: string, type: Type): string { 204 | let code = '' 205 | 206 | if (type instanceof VectorType) 207 | code += `${value} ? ${value}.map(v => ${this.renderVectorSerialize('v', type.item)}) : undefined` 208 | else if (type instanceof CustomType) 209 | code += `${value}.serialize()` 210 | else 211 | code += value 212 | 213 | return code 214 | } 215 | 216 | private isCustomType(type: Type): boolean { 217 | if (type instanceof CustomType) 218 | return true 219 | if (type instanceof VectorType) 220 | return this.isCustomType(type.item) 221 | 222 | return false 223 | } 224 | 225 | private getCustomType(type: Type): CustomType | null { 226 | if (type instanceof VectorType) 227 | return this.getCustomType(type.item) 228 | if (type instanceof CustomType) 229 | return type 230 | 231 | return null 232 | } 233 | } -------------------------------------------------------------------------------- /src/modules/codeGen/infrastructure/SwiftCodeGenerator.ts: -------------------------------------------------------------------------------- 1 | import SourceCode from "../domain/SourceCode"; 2 | import ClassScheme from "../domain/schema/ClassScheme"; 3 | import {CodeGenerator} from "../domain/CodeGenerator"; 4 | import {toCamelCase} from "./Utils"; 5 | import VectorType from "../domain/types/VectorType"; 6 | import CustomType from "../domain/types/CustomType"; 7 | import IntBoolType from "../domain/types/IntBoolType"; 8 | import {Type} from "../domain/types/Type"; 9 | import AnyType from "../domain/types/AnyType"; 10 | import NumberType from "../domain/types/NumberType"; 11 | import StringType from "../domain/types/StringType"; 12 | import BooleanType from "../domain/types/BooleanType"; 13 | import ApiMethodScheme from "../domain/schema/ApiMethodScheme"; 14 | 15 | export default class SwiftCodeGenerator implements CodeGenerator { 16 | public generateClass(scheme: ClassScheme): SourceCode { 17 | let code = new SourceCode() 18 | let props = this.generateProps(scheme) 19 | let constructor = this.generateClassConstructor(scheme) 20 | let deserializeMethod = this.generateDeserializeMethod(scheme) 21 | let serializeMethod = this.generateSerializeMethod(scheme) 22 | 23 | code.add(`class ${scheme.name} {`) 24 | code.append(props) 25 | code.add('') 26 | code.append(constructor, 1) 27 | code.add('') 28 | code.append(deserializeMethod, 1) 29 | code.add('') 30 | code.append(serializeMethod, 1) 31 | code.add('}') 32 | 33 | return code 34 | } 35 | 36 | public generateApiMethod(scheme: ApiMethodScheme): SourceCode { 37 | throw new Error('Not implemented') 38 | } 39 | 40 | public generateApiMethodParamsInterface(scheme: ApiMethodScheme): SourceCode { 41 | throw new Error('Not implemented') 42 | } 43 | 44 | private generateProps(scheme: ClassScheme): SourceCode { 45 | let code = new SourceCode() 46 | 47 | scheme.fields.forEach((field, index) => { 48 | code.add(`public var ${toCamelCase(field.name)}: ${this.renderType(field.type)}`, 1) 49 | }) 50 | 51 | return code 52 | } 53 | 54 | private generateClassConstructor(scheme: ClassScheme): SourceCode { 55 | let code = new SourceCode() 56 | 57 | code.add('init (') 58 | 59 | scheme.fields.forEach((field, index) => { 60 | let coma = this.genComa(scheme.fields, index) 61 | 62 | code.add(`${toCamelCase(field.name)}: ${this.renderType(field.type)}${coma}`, 1) 63 | }) 64 | 65 | code.add(') {') 66 | scheme.fields.forEach((field, index) => { 67 | code.add(`self.${toCamelCase(field.name)} = ${toCamelCase(field.name)}`, 1) 68 | }) 69 | code.add('}') 70 | 71 | return code 72 | } 73 | 74 | private generateDeserializeMethod(scheme: ClassScheme): SourceCode { 75 | let code = new SourceCode() 76 | 77 | code.add(`public static func deserialize(raw: [String: Any]?) -> ${scheme.name}? {`) 78 | code.add(`guard let raw = raw else {`, 1) 79 | code.add(`return nil`, 2) 80 | code.add(`}`, 1) 81 | code.add('') 82 | code.add(`return ${scheme.name} (`, 1) 83 | 84 | scheme.fields.forEach((field, index) => { 85 | let coma = this.genComa(scheme.fields, index) 86 | let fieldVar = `${toCamelCase(field.name)}: raw["${field.name}"] as? ${this.renderNonOptionalType(field.type)}` 87 | 88 | if (field.type instanceof VectorType) 89 | code.add(this.renderVectorDeserialize(`${toCamelCase(field.name)}: (raw["${field.name}"] as? [Any])`, field.type) + coma, 2) 90 | else if (field.type instanceof CustomType) 91 | code.add(`${toCamelCase(field.name)}: ${field.type.name}.deserialize(raw: raw["${field.name}"] as? [String : Any])${coma}`, 2) 92 | else if (field.type instanceof IntBoolType) 93 | code.add(`!!${fieldVar}${coma}`, 2) 94 | else 95 | code.add(fieldVar + coma, 2) 96 | }) 97 | 98 | code.add(`)`, 1) 99 | code.add('}') 100 | 101 | return code 102 | } 103 | 104 | private generateSerializeMethod(scheme: ClassScheme): SourceCode { 105 | let code = new SourceCode() 106 | 107 | code.add(`public func serialize() -> [String: Any] {`) 108 | 109 | code.add(`return [`, 1) 110 | 111 | 112 | scheme.fields.forEach((field, index) => { 113 | let coma = this.genComa(scheme.fields, index) 114 | let fieldVar = `"${field.name}": self.${toCamelCase(field.name)}` 115 | 116 | if (field.type instanceof VectorType) 117 | code.add(`"${field.name}": ${this.renderVectorSerialize(`self.${toCamelCase(field.name)}?`, field.type) + coma}`, 2) 118 | else if (field.type instanceof CustomType) 119 | code.add(`${fieldVar}?.serialize()${coma}`, 2) 120 | else 121 | code.add(`${fieldVar}${coma}`, 2) 122 | }) 123 | 124 | 125 | code.add(']', 1) 126 | code.add('}') 127 | 128 | return code 129 | } 130 | 131 | private renderType(type: Type, withoutUndefined = false): string { 132 | if (type instanceof StringType) 133 | return 'String?' 134 | 135 | if (type instanceof NumberType) 136 | return 'Int?' 137 | 138 | // if (type instanceof FloatType) 139 | // return 'Float?' 140 | 141 | if (type instanceof AnyType) 142 | return 'Any?' 143 | 144 | if (type instanceof BooleanType) 145 | return 'Bool?' 146 | 147 | if (type instanceof IntBoolType) 148 | return 'Bool?' 149 | 150 | if (type instanceof CustomType) 151 | return type.name + `${!withoutUndefined ? '?' : ''}` 152 | 153 | if (type instanceof VectorType) { 154 | return '[' + this.renderNonOptionalType(type.item) + `]${!withoutUndefined ? '?' : ''}` 155 | } 156 | 157 | throw new Error('UNSUPPORTED TYPE' + JSON.stringify(type)) 158 | } 159 | 160 | private renderNonOptionalType(type: Type): string { 161 | if (type instanceof StringType) 162 | return 'String' 163 | 164 | if (type instanceof NumberType) 165 | return 'Int' 166 | 167 | // if (type instanceof FloatType) 168 | // return 'Float' 169 | 170 | if (type instanceof AnyType) 171 | return 'Any' 172 | 173 | if (type instanceof BooleanType) 174 | return 'Bool' 175 | 176 | if (type instanceof IntBoolType) 177 | return 'Bool' 178 | 179 | if (type instanceof CustomType) 180 | return type.name 181 | 182 | if (type instanceof VectorType) { 183 | return '[' + this.renderNonOptionalType(type.item) + ']' 184 | } 185 | 186 | throw new Error('UNSUPPORTED TYPE' + JSON.stringify(type)) 187 | } 188 | 189 | private genComa(list: any[], index: number): string { 190 | return (index == list.length - 1) ? '' : ',' 191 | } 192 | 193 | private renderVectorDeserialize(value: string, type: Type): string { 194 | let code = '' 195 | 196 | if (type instanceof VectorType) 197 | code += `${value}?.flatMap({${this.renderVectorDeserialize('$0', type.item)}})` 198 | else if (type instanceof CustomType) 199 | code += `${type.name}.deserialize(raw: ${value} as? [String: Any])` 200 | else 201 | code += `${value} as? ${this.renderNonOptionalType(type)}` 202 | 203 | return code 204 | } 205 | 206 | private renderVectorSerialize(value: string, type: Type): string { 207 | let code = '' 208 | 209 | if (type instanceof VectorType) 210 | code += `${value}.map({${this.renderVectorSerialize('$0', type.item)}})` 211 | else if (type instanceof CustomType) 212 | code += `${value}.serialize()` 213 | else 214 | code += value 215 | 216 | return code 217 | } 218 | 219 | private isCustomType(type: Type): boolean { 220 | if (type instanceof CustomType) 221 | return true 222 | if (type instanceof VectorType) 223 | return this.isCustomType(type.item) 224 | 225 | return false 226 | } 227 | 228 | private getCustomType(type: Type): CustomType | null { 229 | if (type instanceof VectorType) 230 | return this.getCustomType(type.item) 231 | if (type instanceof CustomType) 232 | return type 233 | 234 | return null 235 | } 236 | } -------------------------------------------------------------------------------- /src/modules/codeGen/infrastructure/TypescriptCodeGenerator.ts: -------------------------------------------------------------------------------- 1 | import {CodeGenerator} from "../domain/CodeGenerator"; 2 | import ClassScheme from "../domain/schema/ClassScheme"; 3 | import SourceCode from "../domain/SourceCode"; 4 | import StringType from "../domain/types/StringType"; 5 | import NumberType from "../domain/types/NumberType"; 6 | import AnyType from "../domain/types/AnyType"; 7 | import BooleanType from "../domain/types/BooleanType"; 8 | import CustomType from "../domain/types/CustomType"; 9 | import {Type} from "../domain/types/Type"; 10 | import VectorType from "../domain/types/VectorType"; 11 | import {toCamelCase} from "./Utils"; 12 | import ApiMethodScheme from "../domain/schema/ApiMethodScheme"; 13 | import IntBoolType from "../domain/types/IntBoolType"; 14 | 15 | export default class TypescriptCodeGenerator implements CodeGenerator { 16 | public generateClass(scheme: ClassScheme): SourceCode { 17 | let code = new SourceCode() 18 | let constructor = this.generateClassConstructor(scheme) 19 | let deserializeMethod = this.generateDeserializeMethod(scheme) 20 | let serializeMethod = this.generateSerializeMethod(scheme) 21 | 22 | code.add(`export class ${scheme.name} {`) 23 | code.append(constructor, 1) 24 | code.add('') 25 | code.append(deserializeMethod, 1) 26 | code.add('') 27 | code.append(serializeMethod, 1) 28 | code.add('}') 29 | 30 | return code 31 | } 32 | 33 | public generateApiMethod(scheme: ApiMethodScheme): SourceCode { 34 | let code = new SourceCode() 35 | 36 | let methodName = toCamelCase(scheme.name, false, '.') 37 | let propsName = `MethodsProps.${toCamelCase(scheme.name, true, '.')}Params` 38 | let responseName = this.renderType(scheme.responseType, true) 39 | 40 | 41 | /** 42 | * Returns detailed information on users. 43 | * 44 | * 45 | * @param {{ 46 | * subview:string, 47 | * el:(number|Element) 48 | * }} params 49 | */ 50 | 51 | code.add(`/**`) 52 | code.add(` * ${scheme.description}`) 53 | code.add(' *') 54 | code.add(' * @param {{') 55 | scheme.params.forEach((param, index) => { 56 | let coma = this.genComa(scheme.params, index) 57 | 58 | code.add(` * ${toCamelCase(param.name)}: (${this.renderType(param.type, true)}${param.required ? '' : '|undefined'})${coma}`) 59 | }) 60 | code.add(' * }} params') 61 | code.add(' *') 62 | code.add(` * @returns {Promise<${responseName}>}`) 63 | code.add(` */`) 64 | code.add(`public async ${methodName}(params: ${propsName}): Promise {`) 65 | code.add('return this.call(', 1) 66 | code.add(`'${scheme.name}',`, 2) 67 | code.add(`{`, 2) 68 | scheme.params.forEach((param, index) => { 69 | let coma = this.genComa(scheme.params, index) 70 | let fieldVar = `${param.name}: params.${param.name}` 71 | 72 | if (param.type instanceof VectorType) 73 | code.add(`${param.name}: ${this.renderVectorSerialize(`params.${param.name}`, param.type) + coma}`, 3) 74 | else if (param.type instanceof CustomType) 75 | code.add(`${fieldVar} ? params.${param.name}.serialize() : undefined${coma}`, 3) 76 | else 77 | code.add(fieldVar + coma, 3) 78 | 79 | 80 | 81 | //code.add(`${param.name}: params.${toCamelCase(param.name)}${coma}`, 3) 82 | }) 83 | code.add(`},`, 2) 84 | code.add(`Responses.${responseName}`, 2) 85 | code.add(')', 1) 86 | code.add('}') 87 | 88 | return code 89 | } 90 | 91 | public generateApiMethodParamsInterface(scheme: ApiMethodScheme): SourceCode { 92 | let code = new SourceCode() 93 | 94 | code.add(`export interface ${toCamelCase(scheme.name, true, '.')}Params {`) 95 | 96 | scheme.params.forEach((prop, index) => { 97 | let coma = this.genComa(scheme.params, index) 98 | let isCustom = this.isCustomType(prop.type) 99 | 100 | // code.add(`/**`, 1) 101 | // code.add(` * ${prop.description}`, 1) 102 | // code.add(` */`, 1) 103 | code.add(`${toCamelCase(prop.name)}${prop.required ? '' : '?'}: ${isCustom ? 'Models.' : ''}${this.renderType(prop.type, true)}${coma}`, 1) 104 | }) 105 | 106 | code.add('}') 107 | 108 | return code 109 | } 110 | 111 | private generateClassConstructor(scheme: ClassScheme): SourceCode { 112 | let code = new SourceCode() 113 | let jsdoc = this.generateClassConstructorJSDoc(scheme) 114 | 115 | code.append(jsdoc) 116 | 117 | code.add('constructor (') 118 | 119 | scheme.fields.forEach((field, index) => { 120 | let coma = this.genComa(scheme.fields, index) 121 | 122 | code.add(`readonly ${toCamelCase(field.name)}: ${this.renderType(field.type)}${coma}`, 1) 123 | }) 124 | 125 | code.add(') {') 126 | code.add('') 127 | code.add('}') 128 | 129 | return code 130 | } 131 | 132 | private generateClassConstructorJSDoc(scheme: ClassScheme): SourceCode { 133 | let code = new SourceCode() 134 | 135 | code.add('/**') 136 | code.add(' * @class') 137 | 138 | 139 | scheme.fields.forEach(field => { 140 | let type = this.removeNamespaceFromCustomType(field.type) 141 | 142 | 143 | code.add(` * @property {${this.renderType(type)}} ${toCamelCase(field.name)} ${field.description}`) 144 | }) 145 | 146 | code.add(' */') 147 | 148 | return code 149 | } 150 | 151 | private generateDeserializeMethod(scheme: ClassScheme): SourceCode { 152 | let code = new SourceCode() 153 | 154 | code.add('/**') 155 | code.add(' * @param {Object} raw') 156 | code.add(` * @returns {${scheme.name}}`) 157 | code.add(' */') 158 | 159 | code.add(`static deserialize(raw: any): ${scheme.name} {`) 160 | code.add(`return new ${scheme.name} (`, 1) 161 | 162 | scheme.fields.forEach((field, index) => { 163 | let coma = this.genComa(scheme.fields, index) 164 | let fieldVar = `raw['${field.name}']` 165 | 166 | if (field.type instanceof VectorType) 167 | code.add(this.renderVectorDeserialize(fieldVar, field.type) + coma, 2) 168 | else if (field.type instanceof CustomType) 169 | code.add(`${fieldVar} ? ${field.type.name}.deserialize(${fieldVar}) : undefined${coma}`, 2) 170 | else if (field.type instanceof IntBoolType) 171 | code.add(`!!${fieldVar}${coma}`, 2) 172 | else 173 | code.add(fieldVar + coma, 2) 174 | }) 175 | 176 | code.add(`)`, 1) 177 | code.add('}') 178 | 179 | return code 180 | } 181 | 182 | private generateSerializeMethod(scheme: ClassScheme): SourceCode { 183 | let code = new SourceCode() 184 | 185 | code.add('/**') 186 | code.add(` * @returns {Object}`) 187 | code.add(' */') 188 | 189 | code.add(`public serialize(): Object {`) 190 | 191 | code.add(`return {`, 1) 192 | 193 | 194 | scheme.fields.forEach((field, index) => { 195 | let coma = this.genComa(scheme.fields, index) 196 | let fieldVar = `${field.name}: this.${field.name}` 197 | 198 | if (field.type instanceof VectorType) 199 | code.add(`${field.name}: ${this.renderVectorSerialize(`this.${field.name}`, field.type) + coma}`, 2) 200 | else if (field.type instanceof CustomType) 201 | code.add(`${fieldVar} ? this.${field.name}.serialize() : undefined${coma}`, 2) 202 | else 203 | code.add(fieldVar + coma, 2) 204 | }) 205 | 206 | 207 | code.add('}', 1) 208 | code.add('}') 209 | 210 | return code 211 | } 212 | 213 | private renderType(type: Type, withoutUndefined = false): string { 214 | if (type instanceof StringType) 215 | return 'string' 216 | 217 | if (type instanceof NumberType) 218 | return 'number' 219 | 220 | if (type instanceof AnyType) 221 | return 'any' 222 | 223 | if (type instanceof BooleanType) 224 | return 'boolean' 225 | 226 | if (type instanceof IntBoolType) 227 | return 'boolean' 228 | 229 | if (type instanceof CustomType) 230 | return type.name + `${!withoutUndefined ? '|undefined' : ''}` 231 | 232 | if (type instanceof VectorType) { 233 | return this.renderType(type.item, true) + `[]${!withoutUndefined ? '|undefined' : ''}` 234 | } 235 | 236 | throw new Error('UNSUPPORTED TYPE' + JSON.stringify(type)) 237 | } 238 | 239 | private genComa(list: any[], index: number): string { 240 | return (index == list.length - 1) ? '' : ',' 241 | } 242 | 243 | private renderVectorDeserialize(value: string, type: Type): string { 244 | let code = '' 245 | 246 | if (type instanceof VectorType) 247 | code += `${value} ? ${value}.map((v: any) => ${this.renderVectorDeserialize('v', type.item)}) : undefined` 248 | else if (type instanceof CustomType) 249 | code += `${value} ? ${type.name}.deserialize(${value}) : undefined` 250 | else 251 | code += value 252 | 253 | return code 254 | } 255 | 256 | private renderVectorSerialize(value: string, type: Type): string { 257 | let code = '' 258 | 259 | if (type instanceof VectorType) 260 | code += `${value} ? ${value}.map((v: any) => ${this.renderVectorSerialize('v', type.item)}) : undefined` 261 | else if (type instanceof CustomType) 262 | code += `${value} ? ${value}.serialize() : undefined` 263 | else 264 | code += value 265 | 266 | return code 267 | } 268 | 269 | private isCustomType(type: Type): boolean { 270 | if (type instanceof CustomType) 271 | return true 272 | if (type instanceof VectorType) 273 | return this.isCustomType(type.item) 274 | 275 | return false 276 | } 277 | 278 | private removeNamespaceFromCustomType(type: Type): Type { 279 | if (type instanceof CustomType) 280 | return new CustomType(type.name.replace('Models.', '')) 281 | if (type instanceof VectorType) 282 | return new VectorType(this.removeNamespaceFromCustomType(type.item)) 283 | 284 | return type 285 | } 286 | } -------------------------------------------------------------------------------- /src/modules/codeGen/infrastructure/Utils.ts: -------------------------------------------------------------------------------- 1 | export type Wrapper = { [P in keyof T]: T[P] } 2 | 3 | export function toCamelCase(str: string, capitalize: boolean = false, separator = '_') { 4 | const parts = str.split(separator) 5 | 6 | if (!parts.length) return str 7 | 8 | const capitalized = parts.slice(1).map(part => part[0].toUpperCase() + part.substr(1)) 9 | 10 | capitalized.unshift(parts[0]) 11 | 12 | let result = capitalized.join('') 13 | 14 | if (capitalize) 15 | return result[0].toUpperCase() + result.slice(1) 16 | 17 | return result 18 | } -------------------------------------------------------------------------------- /src/modules/doc/domain/BaseApiDocGenerator.ts: -------------------------------------------------------------------------------- 1 | import ApiSchema from "../../schemeGenerator/domain/ApiSchema"; 2 | import SourceCode from "../../codeGen/domain/SourceCode"; 3 | 4 | export interface BaseApiDocGenerator { 5 | generateApiDoc(scheme: ApiSchema, schemeVersion: number): SourceCode 6 | } -------------------------------------------------------------------------------- /src/modules/doc/infrastructure/ApiDocGenerator.ts: -------------------------------------------------------------------------------- 1 | import {BaseApiDocGenerator} from "../domain/BaseApiDocGenerator"; 2 | import SourceCode from "../../codeGen/domain/SourceCode"; 3 | import ApiSchema from "../../schemeGenerator/domain/ApiSchema"; 4 | import {Type} from "../../codeGen/domain/types/Type"; 5 | import NumberType from "../../codeGen/domain/types/NumberType"; 6 | import StringType from "../../codeGen/domain/types/StringType"; 7 | import CustomType from "../../codeGen/domain/types/CustomType"; 8 | import BooleanType from "../../codeGen/domain/types/BooleanType"; 9 | import VectorType from "../../codeGen/domain/types/VectorType"; 10 | import TypescriptCodeGenerator from "../../codeGen/infrastructure/TypescriptCodeGenerator"; 11 | import JavaScriptCodeGenerator from "../../codeGen/infrastructure/JavaScriptCodeGenerator"; 12 | import SwiftCodeGenerator from "../../codeGen/infrastructure/SwiftCodeGenerator"; 13 | 14 | export default class ApiDocGenerator implements BaseApiDocGenerator { 15 | private tsCodeGenerator = new TypescriptCodeGenerator() 16 | private jsCodeGenerator = new JavaScriptCodeGenerator() 17 | private swiftCodeGenerator = new SwiftCodeGenerator() 18 | 19 | public generateApiDoc(scheme: ApiSchema, schemeVersion: number): SourceCode { 20 | let code = new SourceCode() 21 | 22 | code.add('') 23 | code.add('') 24 | code.add(``) 30 | code.add(``) 31 | code.add(` 35 | `) 36 | code.add(`V${schemeVersion} API Docs`) 37 | code.add('') 38 | 39 | 40 | code.add('
') 41 | code.add('
') 42 | code.add('
') 43 | code.add('
') 44 | 45 | code.add(`

API Docs

`) 46 | code.add(`

Scheme version: ${schemeVersion}, Generated at ${(new Date()).toString()}

`) 47 | code.add(`
`) 48 | 49 | code.add('

Methods

') 50 | 51 | for (let method of scheme.methods) { 52 | code.add('
') 53 | code.add(`
${method.name}
`) 54 | code.add('
') 55 | 56 | code.add(`

/${method.name}

`) 57 | 58 | code.add('') 59 | code.add('') 60 | code.add('') 61 | code.add('') 62 | code.add('') 63 | code.add('') 64 | code.add('') 65 | 66 | code.add('') 67 | 68 | for (let param of method.params) { 69 | 70 | code.add('') 71 | code.add(``) 72 | code.add(``) 73 | 74 | code.add('') 75 | } 76 | 77 | 78 | code.add('') 79 | code.add('
ParamType
${param.name}${this.renderType(param.type)}
') 80 | 81 | 82 | 83 | let responseType = scheme.responses.find(r => 84 | r.name == (method.responseType as CustomType).name 85 | ) 86 | 87 | if (!responseType) 88 | throw new Error('Cant find response type' + (method.responseType as CustomType).name) 89 | 90 | code.add(`

Response: ${responseType.name}

`) 91 | 92 | code.add('') 93 | code.add('') 94 | code.add('') 95 | code.add('') 96 | code.add('') 97 | code.add('') 98 | code.add('') 99 | 100 | code.add('') 101 | 102 | 103 | for (let param of responseType.fields) { 104 | code.add('') 105 | code.add(``) 106 | code.add(``) 107 | 108 | code.add('') 109 | } 110 | 111 | 112 | code.add('') 113 | code.add('
FieldType
${param.name}${this.renderType(param.type)}
') 114 | 115 | code.add('
') 116 | code.add('
') 117 | } 118 | 119 | code.add('

Models

') 120 | 121 | 122 | 123 | for (let model of scheme.models) { 124 | code.add(`
`) 125 | code.add(`
${model.name}
`) 126 | code.add('
') 127 | 128 | code.add(`

${model.name}

`) 129 | 130 | code.add('') 131 | code.add('') 132 | code.add('') 133 | code.add('') 134 | code.add('') 135 | code.add('') 136 | code.add('') 137 | 138 | code.add('') 139 | 140 | for (let param of model.fields) { 141 | 142 | code.add('') 143 | code.add(``) 144 | code.add(``) 145 | 146 | code.add('') 147 | } 148 | 149 | 150 | code.add('') 151 | code.add('
ParamType
${param.name}${this.renderType(param.type)}
') 152 | 153 | 154 | 155 | let tsModelCode = new SourceCode() 156 | tsModelCode.add('
')
157 |             tsModelCode.add(this.tsCodeGenerator.generateClass(model).render())
158 |             tsModelCode.add('
') 159 | code.append(this.createCollapsablePanel('TS model code', tsModelCode)) 160 | 161 | let jsModelCode = new SourceCode() 162 | jsModelCode.add('
')
163 |             jsModelCode.add(this.jsCodeGenerator.generateClass(model).render())
164 |             jsModelCode.add('
') 165 | code.append(this.createCollapsablePanel('JS model code', jsModelCode)) 166 | 167 | let swiftModelCode = new SourceCode() 168 | swiftModelCode.add('
')
169 |             swiftModelCode.add(this.swiftCodeGenerator.generateClass(model).render())
170 |             swiftModelCode.add('
') 171 | code.append(this.createCollapsablePanel('Swift model code', swiftModelCode)) 172 | 173 | code.add('
') 174 | code.add('
') 175 | } 176 | 177 | 178 | code.add('
') 179 | code.add('
') 180 | code.add('
') 181 | code.add('
') 182 | 183 | 184 | code.add('') 185 | 186 | return code 187 | } 188 | 189 | private renderType(type: Type): string { 190 | if (type instanceof NumberType) 191 | return 'number' 192 | else if (type instanceof StringType) 193 | return 'string' 194 | else if (type instanceof CustomType) 195 | return `${type.name}` 196 | else if (type instanceof BooleanType) 197 | return 'bool' 198 | else if (type instanceof VectorType) 199 | return this.renderType(type.item) + '[ ]' 200 | 201 | throw new Error('Unknown type') 202 | } 203 | 204 | private createCollapsablePanel(name: string, content: SourceCode): SourceCode { 205 | let code = new SourceCode() 206 | let randomId = Math.random().toString(16).replace('.', '') 207 | 208 | code.add('
') 209 | code.add('
') 210 | code.add('
') 211 | code.add('

') 212 | code.add(`${name}`) 213 | code.add('

') 214 | code.add('
') 215 | 216 | 217 | code.add(`
`) 218 | code.add('
') 219 | code.append(content) 220 | code.add('
') 221 | code.add('
') 222 | code.add('
') 223 | code.add('
') 224 | 225 | return code 226 | } 227 | } -------------------------------------------------------------------------------- /src/modules/doc/presentation/DocsGeneratorApplication.ts: -------------------------------------------------------------------------------- 1 | import BaseConsoleApplication from "../../utils/BaseConsoleApplication"; 2 | import ConsoleLogger from "../../logger/infrustructure/ConsoleLogger"; 3 | import ApiSchema from "../../schemeGenerator/domain/ApiSchema"; 4 | import {mkdirSync, readdirSync} from "fs"; 5 | import ApiDocGenerator from "../infrastructure/ApiDocGenerator"; 6 | 7 | class DocsGeneratorApplication extends BaseConsoleApplication { 8 | constructor() { 9 | super() 10 | 11 | try { 12 | this.run() 13 | } 14 | catch (e) { 15 | console.log(e) 16 | } 17 | } 18 | 19 | private async run() { 20 | let args: any = this.getArgs() 21 | 22 | let schemePath = args['s'] 23 | let outPath = args['o'] 24 | 25 | if (!schemePath || !outPath) 26 | this.die('usage: --s= --o=') 27 | 28 | let logger = new ConsoleLogger() 29 | 30 | let docGenerator = new ApiDocGenerator() 31 | 32 | let schemeFiles = await this.readDir(schemePath) 33 | schemeFiles = schemeFiles.filter(f => f !== '.DS_Store') 34 | 35 | let schemeVersions = schemeFiles 36 | .map(f => f.replace('api-scheme-v', '').replace('.json', '')) 37 | .sort() 38 | 39 | logger.log('Found schemes', schemeVersions.map(v => 'V'+v).join(', ')) 40 | 41 | let schema = await this.readFile(schemePath + '/' + schemeFiles[schemeFiles.length -1]) 42 | let apiSchema = ApiSchema.deserialize(JSON.parse(schema)) 43 | 44 | let lastSchemeVersion = schemeVersions[schemeVersions.length - 1] 45 | 46 | logger.log('Last scheme:', 'V' + lastSchemeVersion) 47 | 48 | 49 | try { 50 | readdirSync(outPath + `/sdk-v${lastSchemeVersion}/`) 51 | logger.warn(`Already have Doc for V${lastSchemeVersion}, force regenerating`) 52 | } 53 | catch (e) { 54 | 55 | } 56 | 57 | 58 | let docs = docGenerator.generateApiDoc(apiSchema, Number(lastSchemeVersion)) 59 | 60 | let docPath = outPath + `/api-doc-v${lastSchemeVersion}/` 61 | 62 | try { 63 | mkdirSync(docPath) 64 | } 65 | catch (e) { 66 | 67 | } 68 | 69 | await this.saveToFile( 70 | docPath + 'docs.html', 71 | docs.render() 72 | ) 73 | 74 | logger.log('Done') 75 | } 76 | } 77 | 78 | new DocsGeneratorApplication() -------------------------------------------------------------------------------- /src/modules/logger/domain/BaseLogger.ts: -------------------------------------------------------------------------------- 1 | export default interface BaseLogger { 2 | log(prefix: string, data?: any, recursiveDepth?: boolean): void 3 | 4 | warn(prefix: string, data?: any, recursiveDepth?: boolean): void 5 | 6 | error(prefix: string, data?: any, recursiveDepth?: boolean): void 7 | } -------------------------------------------------------------------------------- /src/modules/logger/infrustructure/ConsoleLogger.ts: -------------------------------------------------------------------------------- 1 | import {format, inspect} from 'util' 2 | import BaseLogger from '../domain/BaseLogger' 3 | 4 | class ColorCodes { 5 | static RED = '\x1b[31m' 6 | static CYAN = '\x1b[36m' 7 | static YELLOW = '\x1b[33m' 8 | static RESET = '\x1b[0m' 9 | } 10 | 11 | export default class ConsoleLogger implements BaseLogger { 12 | public log(prefix: string, data?: any, recursiveDepth?: boolean) { 13 | this.prepareLog(ColorCodes.CYAN, 'log', prefix, data, recursiveDepth) 14 | } 15 | 16 | public warn(prefix: string, data?: any, recursiveDepth?: boolean) { 17 | this.prepareLog(ColorCodes.YELLOW, 'warn', prefix, data, recursiveDepth) 18 | } 19 | 20 | public error(prefix: string, data?: any, recursiveDepth?: boolean) { 21 | this.prepareLog(ColorCodes.RED, 'error', prefix, data, recursiveDepth) 22 | } 23 | 24 | private prepareLog( 25 | color: ColorCodes, 26 | tag: string, 27 | prefix: string, 28 | data?: any, 29 | recursiveDepth?: boolean 30 | ) { 31 | const time = new Date().toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, '$1') 32 | 33 | if (data && recursiveDepth == true) { 34 | process.stdout.write(`${time} ${color}[${tag}]${ColorCodes.RESET} ${prefix} ${inspect(data || '', { depth: null })} \n`) 35 | 36 | return 37 | } 38 | 39 | process.stdout.write(`${time} ${color}[${tag}]${ColorCodes.RESET} ${prefix} ${format(data || '')} \n`) 40 | } 41 | } -------------------------------------------------------------------------------- /src/modules/schemeGenerator/application/AirshipSchemeGenerator.ts: -------------------------------------------------------------------------------- 1 | import AirshipApiSchemeGenerator from "../infrastructure/AirshipApiSchemeGenerator"; 2 | import BaseLogger from "../../logger/domain/BaseLogger"; 3 | import {RequestType, ResponseType} from "../domain/ApiSchemeGenerator"; 4 | import ApiSchema from "../domain/ApiSchema"; 5 | 6 | /** 7 | * AirshipSchemeGenerator Generates API Server scheme 8 | */ 9 | export default class AirshipSchemeGenerator { 10 | private _airshipApiSchemeGenerator: AirshipApiSchemeGenerator 11 | private _logger: BaseLogger 12 | private _methods: [RequestType, ResponseType][] 13 | 14 | constructor( 15 | airshipApiSchemeGenerator: AirshipApiSchemeGenerator, 16 | logger: BaseLogger, 17 | ...methods: [RequestType, ResponseType][] 18 | ) { 19 | this._airshipApiSchemeGenerator = airshipApiSchemeGenerator 20 | this._logger = logger 21 | this._methods = methods 22 | } 23 | 24 | public generate(): ApiSchema { 25 | return this._airshipApiSchemeGenerator.generateApiScheme(...this._methods) 26 | } 27 | } -------------------------------------------------------------------------------- /src/modules/schemeGenerator/domain/ApiSchema.ts: -------------------------------------------------------------------------------- 1 | import ClassScheme from "../../codeGen/domain/schema/ClassScheme"; 2 | import ApiMethodScheme from "../../codeGen/domain/schema/ApiMethodScheme"; 3 | 4 | export default class ApiSchema { 5 | constructor( 6 | readonly models: ClassScheme[], 7 | readonly methods: ApiMethodScheme[], 8 | readonly responses: ClassScheme[] 9 | ) { 10 | 11 | } 12 | 13 | public serialize(): Object { 14 | return { 15 | models: this.models.map(m => m.serialize()), 16 | methods: this.methods.map(m => m.serialize()), 17 | responses: this.responses.map(r => r.serialize()) 18 | } 19 | } 20 | 21 | public static deserialize(raw: any): ApiSchema { 22 | return new ApiSchema( 23 | raw['models'].map(ClassScheme.deserialize), 24 | raw['methods'].map(ApiMethodScheme.deserialize), 25 | raw['responses'].map(ClassScheme.deserialize) 26 | ) 27 | } 28 | } -------------------------------------------------------------------------------- /src/modules/schemeGenerator/domain/ApiSchemeGenerator.ts: -------------------------------------------------------------------------------- 1 | import {ASResponse} from "../../apiServer/domain/entity/ASResponse"; 2 | import {ASRequest} from "../../apiServer/domain/entity/ASRequest"; 3 | import ApiSchema from "./ApiSchema"; 4 | import {ISerializable} from "../../serialize/BaseSerializer"; 5 | 6 | export type RequestType = T 7 | export type ResponseType = T 8 | 9 | export interface ApiSchemeGenerator { 10 | generateApiScheme(...methods: [ RequestType, ResponseType ][]): ApiSchema 11 | 12 | } -------------------------------------------------------------------------------- /src/modules/schemeGenerator/infrastructure/AirshipApiSchemeGenerator.ts: -------------------------------------------------------------------------------- 1 | import {ApiSchemeGenerator, RequestType, ResponseType} from "../domain/ApiSchemeGenerator"; 2 | import ApiMethodScheme from "../../codeGen/domain/schema/ApiMethodScheme"; 3 | import ClassScheme from "../../codeGen/domain/schema/ClassScheme"; 4 | import ApiMethodParam from "../../codeGen/domain/schema/ApiMethodParam"; 5 | import CustomType from "../../codeGen/domain/types/CustomType"; 6 | import ApiSchema from "../domain/ApiSchema"; 7 | import ErrorResponse from "../../apiServer/domain/entity/ASErrorResponse"; 8 | import SuccessResponse from "../../apiServer/domain/entity/ASSuccessResponse"; 9 | import {BaseSerializer, ISerializable} from "../../serialize/BaseSerializer"; 10 | 11 | export default class AirshipApiSchemeGenerator implements ApiSchemeGenerator { 12 | public generateApiScheme(...methods: [RequestType, ResponseType][]): ApiSchema { 13 | let apiMethodsSchemes: ApiMethodScheme[] = [] 14 | let models: ISerializable & Function[] = [] 15 | let responsesSchemes: ClassScheme[] = [] 16 | 17 | methods.forEach(m => { 18 | let [request, response] = m 19 | 20 | apiMethodsSchemes.push(this.getApiMethodScheme(request, response)) 21 | 22 | models.push(...BaseSerializer.getClassDependencies(request)) 23 | 24 | if (response != ErrorResponse && response != SuccessResponse) { 25 | models.push(...BaseSerializer.getClassDependencies(response)) 26 | responsesSchemes.push(BaseSerializer.getClassScheme(response)) 27 | } 28 | }) 29 | 30 | responsesSchemes.push(BaseSerializer.getClassScheme(ErrorResponse)) 31 | models.push(...BaseSerializer.getClassDependencies(ErrorResponse)) 32 | 33 | responsesSchemes.push(BaseSerializer.getClassScheme(SuccessResponse)) 34 | models.push(...BaseSerializer.getClassDependencies(SuccessResponse)) 35 | 36 | return new ApiSchema( 37 | Array.from(new Set(models)).map(m => BaseSerializer.getClassScheme(m)), 38 | apiMethodsSchemes, 39 | responsesSchemes 40 | ) 41 | } 42 | 43 | private getApiMethodScheme( 44 | request: RequestType, 45 | response: ResponseType 46 | ): ApiMethodScheme { 47 | let queryPath = request.getQueryPath() 48 | let requestScheme = BaseSerializer.getClassScheme(request) 49 | 50 | let params: ApiMethodParam[] = requestScheme.fields.map(field => { 51 | return new ApiMethodParam( 52 | field.name, 53 | field.type, 54 | true, 55 | field.description 56 | ) 57 | }) 58 | 59 | let methodScheme = new ApiMethodScheme( 60 | queryPath 61 | .split('/') 62 | .map((name: string, index: number) => { 63 | if (index == 0 || index == 1) 64 | return name 65 | 66 | return `${name[0].toUpperCase()}${name.slice(1)}` 67 | }) 68 | .join(''), 69 | params, 70 | new CustomType(response.prototype.constructor.name), 71 | '' 72 | ) 73 | 74 | return methodScheme 75 | } 76 | } -------------------------------------------------------------------------------- /src/modules/schemeGenerator/presentation/AirshipSchemeGeneratorApplication.ts: -------------------------------------------------------------------------------- 1 | import BaseConsoleApplication from "../../utils/BaseConsoleApplication"; 2 | import ConsoleLogger from "../../logger/infrustructure/ConsoleLogger"; 3 | import AirshipSchemeGenerator from "../application/AirshipSchemeGenerator"; 4 | import AirshipApiSchemeGenerator from "../infrastructure/AirshipApiSchemeGenerator"; 5 | import {deepEqual} from "assert"; 6 | import {ApiServerConfig} from "../../apiServer/domain/ServerConfig"; 7 | 8 | class AirshipSchemeGeneratorApplication extends BaseConsoleApplication { 9 | constructor() { 10 | super() 11 | 12 | try { 13 | this.run() 14 | } 15 | catch (e) { 16 | console.error(e) 17 | } 18 | } 19 | 20 | private async run() { 21 | let logger = new ConsoleLogger() 22 | 23 | let airshipApiSchemeGenerator = new AirshipApiSchemeGenerator() 24 | 25 | 26 | let args = this.getArgs() 27 | let path = args['o'] 28 | let configPath = args['c'] 29 | 30 | 31 | if (!path || !configPath) 32 | this.die(`usage: --o= --c=\n\noutput_path: absolute output path\nconfig: absolute path to config`) 33 | 34 | let config: ApiServerConfig = require(configPath).default 35 | 36 | let schemeFiles = await this.readDir(path) 37 | 38 | let versions = schemeFiles 39 | .filter(f => f !== '.DS_Store') 40 | .map(f => f.replace('api-scheme-v', '').replace('.json', '')) 41 | .sort() 42 | 43 | 44 | let generator = new AirshipSchemeGenerator( 45 | airshipApiSchemeGenerator, 46 | logger, 47 | 48 | ...config.endpoints 49 | ) 50 | 51 | let v = 1 52 | 53 | try { 54 | let scheme = generator.generate() 55 | } 56 | catch (e) { 57 | console.log(e) 58 | } 59 | 60 | let scheme = generator.generate() 61 | 62 | if (versions.length > 0) { 63 | v = parseInt(versions[versions.length - 1]) + 1 64 | let lastScheme = await this.readFile(path + '/api-scheme-v' + versions[versions.length - 1] + '.json') 65 | 66 | try { 67 | deepEqual(JSON.parse(lastScheme), scheme.serialize()) 68 | logger.warn('scheme-generator', `scheme was not changed since v${v-1}`) 69 | } 70 | catch (e) { 71 | v = parseInt(versions[versions.length - 1]) + 1 72 | 73 | await this.saveToFile(path + '/api-scheme-v' + v + '.json', JSON.stringify(scheme.serialize())) 74 | 75 | logger.log('scheme-generator', `scheme v${v} generated`) 76 | } 77 | } 78 | else { 79 | await this.saveToFile(path + '/api-scheme-v' + v + '.json', JSON.stringify(scheme.serialize())) 80 | logger.log('scheme-generator', `scheme v${v} generated`) 81 | } 82 | } 83 | } 84 | 85 | new AirshipSchemeGeneratorApplication() -------------------------------------------------------------------------------- /src/modules/sdkGenerator/application/AirshipSDKGenerator.ts: -------------------------------------------------------------------------------- 1 | import {ApiSDKGenerator} from "../domain/ApiSDKGenerator"; 2 | import BaseLogger from "../../logger/domain/BaseLogger"; 3 | import ApiSchema from "../../schemeGenerator/domain/ApiSchema"; 4 | import SDKFile from "../domain/SDKFile"; 5 | import {SDKConfig} from "../domain/SDKConfig"; 6 | 7 | /** 8 | * AirshipSDKGenerator generates ready to use, 9 | * fully statically typed TypeScript SDK for fronted 10 | */ 11 | export default class AirshipSDKGenerator { 12 | private _sdkGenerator: ApiSDKGenerator 13 | private _apiSchema: ApiSchema 14 | private _config: SDKConfig 15 | private _logger: BaseLogger 16 | 17 | constructor( 18 | sdkGenerator: ApiSDKGenerator, 19 | apiSchema: ApiSchema, 20 | config: SDKConfig, 21 | logger: BaseLogger, 22 | ) { 23 | this._sdkGenerator = sdkGenerator 24 | this._apiSchema = apiSchema 25 | this._config = config 26 | this._logger = logger 27 | } 28 | 29 | public generate(): SDKFile[] { 30 | let models = new SDKFile('Models.ts', this._sdkGenerator.generateModelsFile(this._apiSchema)) 31 | let responses = new SDKFile('Responses.ts', this._sdkGenerator.generateResponsesFile(this._apiSchema)) 32 | let methodsProps = new SDKFile('MethodsProps.ts', this._sdkGenerator.generateMethodsProps(this._apiSchema)) 33 | let apiClass = new SDKFile('API.ts', this._sdkGenerator.generateApiClassFile(this._apiSchema, this._config)) 34 | 35 | return [models, responses, methodsProps, apiClass] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/modules/sdkGenerator/domain/ApiSDKGenerator.ts: -------------------------------------------------------------------------------- 1 | import SourceCode from "../../codeGen/domain/SourceCode" 2 | import ApiSchema from "../../schemeGenerator/domain/ApiSchema"; 3 | import {SDKConfig} from "./SDKConfig"; 4 | 5 | export interface ApiSDKGenerator { 6 | generateModelsFile(scheme: ApiSchema): SourceCode 7 | 8 | generateResponsesFile(scheme: ApiSchema): SourceCode 9 | 10 | generateMethodsProps(scheme: ApiSchema): SourceCode 11 | 12 | generateApiClassFile(scheme: ApiSchema, config: SDKConfig): SourceCode 13 | } -------------------------------------------------------------------------------- /src/modules/sdkGenerator/domain/SDKConfig.ts: -------------------------------------------------------------------------------- 1 | export interface SDKConfig { 2 | sdkName: string, 3 | apiPath: string, 4 | schemeVersion: number 5 | } -------------------------------------------------------------------------------- /src/modules/sdkGenerator/domain/SDKFile.ts: -------------------------------------------------------------------------------- 1 | import SourceCode from "../../codeGen/domain/SourceCode"; 2 | 3 | export default class SDKFile { 4 | constructor( 5 | readonly fileName: string, 6 | readonly code: SourceCode 7 | ) { 8 | 9 | } 10 | } -------------------------------------------------------------------------------- /src/modules/sdkGenerator/infrastructure/AirshipApiSDKGenerator.ts: -------------------------------------------------------------------------------- 1 | import {ApiSDKGenerator} from "../domain/ApiSDKGenerator"; 2 | import SourceCode from "../../codeGen/domain/SourceCode"; 3 | import {CodeGenerator} from "../../codeGen/domain/CodeGenerator"; 4 | import TypescriptCodeGenerator from "../../codeGen/infrastructure/TypescriptCodeGenerator"; 5 | import ApiSchema from "../../schemeGenerator/domain/ApiSchema"; 6 | import {SDKConfig} from "../domain/SDKConfig"; 7 | import CustomType from "../../codeGen/domain/types/CustomType"; 8 | import ClassScheme from "../../codeGen/domain/schema/ClassScheme"; 9 | import ClassField from "../../codeGen/domain/schema/ClassField"; 10 | import VectorType from "../../codeGen/domain/types/VectorType"; 11 | import {Type} from "../../codeGen/domain/types/Type"; 12 | 13 | export default class AirshipApiSDKGenerator implements ApiSDKGenerator { 14 | private _codeGenerator: CodeGenerator 15 | 16 | constructor() { 17 | this._codeGenerator = new TypescriptCodeGenerator() 18 | } 19 | 20 | public generateModelsFile(scheme: ApiSchema): SourceCode { 21 | let code = new SourceCode() 22 | 23 | scheme.models.forEach(m => { 24 | code.append(this._codeGenerator.generateClass(m)) 25 | code.add('') 26 | }) 27 | 28 | return code 29 | } 30 | 31 | public generateResponsesFile(scheme: ApiSchema): SourceCode { 32 | let code = new SourceCode() 33 | 34 | code.add(`import * as Models from './Models'`) 35 | code.add('') 36 | 37 | function prepareType(type: Type): Type { 38 | if (type instanceof CustomType) 39 | return new CustomType(`Models.${type.name}`) 40 | if (type instanceof VectorType) 41 | return new VectorType(prepareType(type.item)) 42 | 43 | return type 44 | } 45 | 46 | scheme.responses.forEach(r => { 47 | let fields = r.fields.map(f => { 48 | return new ClassField( 49 | f.name, 50 | prepareType(f.type), 51 | f.description 52 | ) 53 | }) 54 | code.append(this._codeGenerator.generateClass(new ClassScheme(r.name, fields))) 55 | code.add('') 56 | }) 57 | 58 | return code 59 | } 60 | 61 | public generateMethodsProps(scheme: ApiSchema): SourceCode { 62 | let code = new SourceCode() 63 | 64 | code.add(`import * as Models from './Models'`) 65 | code.add('') 66 | 67 | scheme.methods.forEach(m => { 68 | code.append(this._codeGenerator.generateApiMethodParamsInterface(m)) 69 | code.add('') 70 | }) 71 | 72 | return code 73 | } 74 | 75 | public generateApiClassFile(scheme: ApiSchema, config: SDKConfig): SourceCode { 76 | let code = new SourceCode() 77 | 78 | code 79 | .add('/**') 80 | .add(' * This is an automatically generated code (and probably compiled with TSC)') 81 | .add(` * Generated at ${(new Date()).toString()}`) 82 | .add(` * Scheme version: ${config.schemeVersion}`) 83 | .add(' */') 84 | .add(`const API_PATH = '${config.apiPath}'`) 85 | .add(``) 86 | .add(`import * as Responses from './Responses'`) 87 | .add(`import * as MethodsProps from './MethodsProps'`) 88 | .add('') 89 | .add(`export default class ${config.sdkName} {`) 90 | .add(`public async call(method: string, params: Object, responseType?: Function): Promise {`, 1) 91 | .add('return fetch(', 2) 92 | .add(`API_PATH + method,`, 3) 93 | .add(`{`, 3) 94 | .add(`method: 'POST',`, 4) 95 | .add(`body: JSON.stringify(params),`, 4) 96 | .add(`headers: {`, 4) 97 | .add(`'Content-Type': 'application/json'`, 5) 98 | .add(`}`, 4) 99 | .add(`}`, 3) 100 | .add(`)`, 2) 101 | .add(`.then(r => {`, 3) 102 | .add(`return r.json()`, 4) 103 | .add(`})`, 3) 104 | .add(`.then(json => {`, 3) 105 | .add(`if (json.ok == false)`, 4) 106 | .add(`throw Responses.ErrorResponse.deserialize(json)`, 5) 107 | .add(`else`, 4) 108 | .add(`return json`, 5) 109 | .add(`})`, 3) 110 | .add(`.then(data => {`, 3) 111 | .add(`if (responseType)`, 4) 112 | .add(`return (responseType as any).deserialize(data)`, 5) 113 | .add('') 114 | .add(`return data`, 4) 115 | .add(`})`, 3) 116 | .add(`}`, 1) 117 | .add('') 118 | 119 | scheme.methods.forEach(m => { 120 | code.append(this._codeGenerator.generateApiMethod(m), 1) 121 | }) 122 | 123 | code.add('}') 124 | 125 | return code 126 | } 127 | } -------------------------------------------------------------------------------- /src/modules/sdkGenerator/presentation/AirshipSDKGeneratorApplication.ts: -------------------------------------------------------------------------------- 1 | import BaseConsoleApplication from "../../utils/BaseConsoleApplication"; 2 | import AirshipSDKGenerator from "../application/AirshipSDKGenerator"; 3 | import AirshipApiSDKGenerator from "../infrastructure/AirshipApiSDKGenerator"; 4 | import ConsoleLogger from "../../logger/infrustructure/ConsoleLogger"; 5 | import ApiSchema from "../../schemeGenerator/domain/ApiSchema"; 6 | import {mkdirSync, readdirSync} from "fs"; 7 | 8 | class AirshipSDKGeneratorApplication extends BaseConsoleApplication { 9 | constructor() { 10 | super() 11 | 12 | try { 13 | this.run() 14 | } 15 | catch (e) { 16 | console.log(e) 17 | } 18 | } 19 | 20 | private async run() { 21 | let args: any = this.getArgs() 22 | let schemePath = args['s'] 23 | let outPath = args['o'] 24 | 25 | if (!schemePath || !outPath) 26 | this.die('usage: --s= --o=') 27 | 28 | 29 | let logger = new ConsoleLogger() 30 | 31 | let schemeFiles = await this.readDir(schemePath) 32 | schemeFiles = schemeFiles.filter(f => f !== '.DS_Store') 33 | 34 | let schemeVersions = schemeFiles 35 | .map(f => 36 | f 37 | .replace('api-scheme-v', '') 38 | .replace('.json', '') 39 | ) 40 | .map(v => parseInt(v, 10)) 41 | .sort((a,b) => a-b) 42 | 43 | console.log(schemeVersions) 44 | 45 | logger.log('Found schemes', schemeVersions.map(v => 'V'+v).join(', ')) 46 | 47 | let lastSchemeVersion = schemeVersions[schemeVersions.length - 1] 48 | 49 | let lastSchemeFileName = schemeFiles.find(file => 50 | file 51 | .replace('api-scheme-v', '') 52 | .replace('.json','') 53 | === String(lastSchemeVersion) 54 | ) 55 | 56 | let schema = await this.readFile(schemePath + '/' + lastSchemeFileName) 57 | let apiSchema = ApiSchema.deserialize(JSON.parse(schema)) 58 | 59 | 60 | 61 | logger.log('Last scheme:', 'V' + lastSchemeVersion) 62 | 63 | try { 64 | readdirSync(outPath + `/sdk-v${lastSchemeVersion}/`) 65 | logger.warn(`Already have SDK for V${lastSchemeVersion}, force generating`) 66 | //this.die('') 67 | } 68 | catch (e) { 69 | 70 | } 71 | 72 | let airshipSDKGenerator = new AirshipSDKGenerator( 73 | new AirshipApiSDKGenerator(), 74 | apiSchema, 75 | { 76 | sdkName: 'AirshipApi', 77 | apiPath: '/api/', 78 | schemeVersion: Number(lastSchemeVersion) 79 | }, 80 | logger 81 | ) 82 | 83 | let sdk = airshipSDKGenerator.generate() 84 | 85 | let sdkPath = outPath + `/sdk-v${lastSchemeVersion}/` 86 | 87 | try { 88 | mkdirSync(sdkPath) 89 | } 90 | catch (e) { 91 | 92 | } 93 | 94 | for (let file of sdk) { 95 | logger.log('Saving', file.fileName) 96 | await this.saveToFile( 97 | sdkPath + file.fileName, 98 | file.code.render() 99 | ) 100 | } 101 | 102 | logger.log('Done') 103 | } 104 | } 105 | 106 | new AirshipSDKGeneratorApplication() -------------------------------------------------------------------------------- /src/modules/serialize/BaseSerializer.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | import VectorType from "../codeGen/domain/types/VectorType"; 3 | import CustomType from "../codeGen/domain/types/CustomType"; 4 | import AnyType from "../codeGen/domain/types/AnyType"; 5 | import ObjectType from "../codeGen/domain/types/ObjectType"; 6 | import BooleanType from "../codeGen/domain/types/BooleanType"; 7 | import NumberType from "../codeGen/domain/types/NumberType"; 8 | import StringType from "../codeGen/domain/types/StringType"; 9 | import {Type} from "../codeGen/domain/types/Type"; 10 | import ClassScheme from "../codeGen/domain/schema/ClassScheme"; 11 | import ClassField from "../codeGen/domain/schema/ClassField"; 12 | 13 | export const SerializableKey = 'SerializableKey' 14 | export const SerializableKeys = 'SerializableKeys' 15 | export const SerializableTypes = 'SerializableTypes' 16 | export const SerializableArrayTypes = 'SerializableArrayTypes' 17 | 18 | export function serializable(name?: string, arrayType?: Function, isAny: boolean = false) { 19 | return (target: Object, propertyKey: string) => { 20 | const constructor: any = target 21 | 22 | name = name || propertyKey.replace('_', '') 23 | 24 | Reflect.defineMetadata(SerializableKey, name, target, propertyKey) 25 | 26 | constructor[SerializableKeys] = constructor[SerializableKeys] || [] 27 | constructor[SerializableTypes] = constructor[SerializableTypes] || [] 28 | constructor[SerializableArrayTypes] = constructor[SerializableArrayTypes] || [] 29 | 30 | let type = Reflect.getMetadata('design:type', target, propertyKey) 31 | 32 | // we need new arrays here, because at this point they are references to super class arrays 33 | // so just pushing new values would modify super (and other child) class array 34 | 35 | constructor[SerializableKeys] = [...constructor[SerializableKeys], name] 36 | constructor[SerializableTypes] = [...constructor[SerializableTypes], (isAny ? 'any' : type)] 37 | constructor[SerializableArrayTypes] = [...constructor[SerializableArrayTypes], arrayType] 38 | } 39 | } 40 | 41 | export interface ISerializable { 42 | 43 | } 44 | 45 | export abstract class BaseSerializer { 46 | public static serialize(entity: ISerializable): Object { 47 | throw new Error('Not implemented') 48 | } 49 | 50 | public static deserialize( 51 | serializableType: ISerializable & Function, 52 | raw: { [key: string]: any }, 53 | valuePath: string[] = [] 54 | ): T { 55 | throw new Error('Not implemented') 56 | } 57 | 58 | public static getClassScheme(serializableType: ISerializable & Function): ClassScheme { 59 | const serializableConstructor = serializableType.prototype 60 | let propsNames: string[] = serializableConstructor[SerializableKeys] 61 | const types = serializableConstructor[SerializableTypes] 62 | const arrayTypes = serializableConstructor[SerializableArrayTypes] 63 | 64 | let fields: ClassField[] = [] 65 | 66 | propsNames = propsNames || [] 67 | 68 | propsNames.forEach((propName, index) => { 69 | fields.push( 70 | new ClassField( 71 | propName, 72 | this.getType(types[index], arrayTypes[index]), 73 | '' 74 | ) 75 | ) 76 | }) 77 | 78 | 79 | return new ClassScheme( 80 | serializableConstructor.constructor.name, 81 | fields 82 | ) 83 | } 84 | 85 | public static getClassDependencies(serializableType: ISerializable & Function): (ISerializable & Function)[] { 86 | const serializableConstructor = serializableType.prototype 87 | const propsNames: string[] = serializableConstructor[SerializableKeys] || [] 88 | const types = serializableConstructor[SerializableTypes] 89 | const arrayTypes = serializableConstructor[SerializableArrayTypes] 90 | 91 | let dependencies: {[key: string]: (ISerializable & Function)} = {} 92 | 93 | let subDeps: any[] = [] 94 | 95 | propsNames.forEach((propName, index) => { 96 | if (this.isSerializableObject(types[index]) == true) { 97 | 98 | 99 | dependencies[types[index].prototype.constructor.name] = types[index] 100 | subDeps = [...subDeps, ...this.getClassDependencies(types[index])] 101 | } 102 | 103 | else if (arrayTypes[index] && this.isSerializableObject(arrayTypes[index]) == true) { 104 | dependencies[arrayTypes[index].prototype.constructor.name] = arrayTypes[index] 105 | subDeps = [...subDeps, ...this.getClassDependencies(arrayTypes[index])] 106 | } 107 | }) 108 | 109 | return [...Object.keys(dependencies).map(key => dependencies[key]), ...subDeps] 110 | } 111 | 112 | protected static isSerializableObject(object: Function): boolean { 113 | if (object as any == 'any') 114 | return false 115 | 116 | return !!object.prototype[SerializableKeys] 117 | } 118 | 119 | protected static getType(propType: any, arrayType?: any): Type { 120 | if (propType == String) 121 | return new StringType() 122 | if (propType == Number) 123 | return new NumberType() 124 | if (propType == Boolean) 125 | return new BooleanType() 126 | if (propType == Object) 127 | return new ObjectType() 128 | if (propType == 'any') 129 | return new AnyType() 130 | if (this.isSerializableObject(propType) == true) 131 | return new CustomType(propType.prototype.constructor.name) 132 | if (propType == Array && arrayType) 133 | return new VectorType(this.getType(arrayType)) 134 | 135 | throw new Error(`Model scheme generation fail, unknown type: ${propType}`) 136 | } 137 | 138 | protected static checkType( 139 | prop: string, 140 | value: any, 141 | expectedType: any, 142 | isSerializable: boolean 143 | ) { 144 | if (value == undefined) 145 | throw new Error(`${prop} argument missing`) 146 | 147 | let valueType = typeof value 148 | 149 | if (expectedType == Array && !Array.isArray(value)) 150 | throw new Error(`${prop} must be array`) 151 | 152 | if (!isSerializable) { 153 | if (expectedType == String && valueType !== 'string') 154 | throw new Error(`${prop} must be string instead of ${valueType}`) 155 | if (expectedType == Number && valueType !== 'number') 156 | throw new Error(`${prop} must be number instead of ${valueType}`) 157 | if (expectedType == Boolean && valueType !== 'boolean') 158 | throw new Error(`${prop} must be boolean instead of ${valueType}`) 159 | if (expectedType == Object && valueType !== 'object') 160 | throw new Error(`${prop} must be boolean instead of ${valueType}`) 161 | } 162 | } 163 | } -------------------------------------------------------------------------------- /src/modules/serialize/JSONSerializer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseSerializer, ISerializable, SerializableArrayTypes, SerializableKey, SerializableKeys, 3 | SerializableTypes 4 | } from "./BaseSerializer"; 5 | 6 | export default class JSONSerializer extends BaseSerializer { 7 | public static deserialize( 8 | serializableType: ISerializable & Function, 9 | raw: { [key: string]: any }, 10 | valuePath: string[] = [] 11 | ): T { 12 | const serializableConstructor = serializableType.prototype 13 | const propsNames: string[] = serializableConstructor[SerializableKeys] || [] 14 | const types = serializableConstructor[SerializableTypes] 15 | const arrayTypes = serializableConstructor[SerializableArrayTypes] 16 | 17 | const props = propsNames.map((prop, index) => { 18 | let type = types[index] 19 | let arrayType = arrayTypes[index] 20 | let isSerializable = super.isSerializableObject(type) == true 21 | 22 | 23 | valuePath.push(prop) 24 | 25 | this.checkType(valuePath.join('.'), raw[prop], type, isSerializable) 26 | 27 | if ( 28 | arrayType && 29 | super.isSerializableObject(arrayType) == true 30 | ) { 31 | let val = raw[prop].map((v: any, i: number) => { 32 | valuePath.push(`{${i}}`) 33 | 34 | let deserialized = JSONSerializer.deserialize(arrayType, v, valuePath) 35 | 36 | valuePath.pop() 37 | 38 | return deserialized 39 | }) 40 | 41 | valuePath.pop() 42 | 43 | return val 44 | } 45 | else if (arrayType) { 46 | let val = raw[prop].map((v: any, i: number) => { 47 | valuePath.push(`{${i}}`) 48 | 49 | super.checkType(valuePath.join('.'), v, arrayType, false) 50 | valuePath.pop() 51 | 52 | return v 53 | }) 54 | valuePath.pop() 55 | 56 | return val 57 | } 58 | else if (isSerializable) { 59 | let val = JSONSerializer.deserialize(type, raw[prop], valuePath) 60 | valuePath.pop() 61 | 62 | return val 63 | } 64 | else { 65 | valuePath.pop() 66 | return raw[prop] 67 | } 68 | }) 69 | 70 | return new serializableConstructor.constructor(...props) 71 | } 72 | 73 | public static serialize(entity: ISerializable): Object { 74 | let result: { [key: string]: any} = {} 75 | 76 | let arrayTypes = (entity as any)[SerializableArrayTypes] 77 | 78 | let i = 0 79 | 80 | for(let prop in entity) { 81 | if ( 82 | prop == SerializableKeys || 83 | prop == SerializableTypes || 84 | prop == SerializableArrayTypes 85 | ) 86 | continue 87 | 88 | let keyName = Reflect.getMetadata(SerializableKey, entity, prop) || prop 89 | let type = Reflect.getMetadata('design:type', entity, prop) 90 | let arrayType = arrayTypes[i] 91 | 92 | if (arrayType && super.isSerializableObject(arrayType) == true) { 93 | result[keyName] = ((entity as any)[prop] as any).map(JSONSerializer.serialize) 94 | } 95 | else if (super.isSerializableObject(type) == true) { 96 | result[keyName] = JSONSerializer.serialize((entity as any)[prop]) 97 | } 98 | else 99 | result[keyName] = (entity as any)[prop] 100 | 101 | i++ 102 | } 103 | 104 | return result 105 | } 106 | } 107 | 108 | -------------------------------------------------------------------------------- /src/modules/statistics/domain/BaseStatisticsCounter.ts: -------------------------------------------------------------------------------- 1 | export abstract class BaseStatisticsCounter { 2 | public abstract countRequestHit(): void 3 | 4 | public abstract doneRequest(): void 5 | } -------------------------------------------------------------------------------- /src/modules/statistics/infrastructure/LocalStatisticsCounter.ts: -------------------------------------------------------------------------------- 1 | import {BaseStatisticsCounter} from "../domain/BaseStatisticsCounter"; 2 | import BaseLogger from "../../logger/domain/BaseLogger"; 3 | import {memoryUsage} from 'process'; 4 | 5 | export default class LocalStatisticsCounter extends BaseStatisticsCounter { 6 | private _logger: BaseLogger 7 | 8 | private _hitsPerSecond: number 9 | 10 | private _hitsPerMinute: number 11 | 12 | private _concurrentRequests: number 13 | 14 | private _allRequests: number 15 | 16 | constructor( 17 | logger: BaseLogger, 18 | silently: boolean = false, 19 | logFrequency: number = 5000 20 | ) { 21 | super() 22 | 23 | this._logger = logger 24 | 25 | this._hitsPerSecond = 0 26 | this._hitsPerMinute = 0 27 | this._concurrentRequests = 0 28 | this._allRequests = 0 29 | 30 | this._logger.log('Local statistics started') 31 | 32 | setInterval(() => { 33 | if (!silently) { 34 | let memUsage = memoryUsage() 35 | 36 | this._logger.log( 37 | 'Local statistics:', 38 | `\n Hits per second: ${this._hitsPerSecond}\n` + 39 | ` Hits per minute: ${this._hitsPerMinute}\n` + 40 | ` All hits: ${this._allRequests}\n` + 41 | ` Concurrent requests: ${this._concurrentRequests}\n` + 42 | ` Total memory in RAM: ${this.toMegabytes(memUsage.rss)} mb\n` + 43 | ` Native modules memory usage: ${this.toMegabytes((memUsage as any).external)} mb\n` + 44 | ` V8 total heap: ${this.toMegabytes(memUsage.heapTotal)} mb\n` + 45 | ` V8 used heap: ${this.toMegabytes(memUsage.heapUsed)} mb\n` 46 | ) 47 | } 48 | 49 | this._hitsPerSecond = 0 50 | }, logFrequency) 51 | 52 | setInterval(() => { 53 | this._hitsPerMinute = 0 54 | }, 1000 * 60) 55 | } 56 | 57 | public countRequestHit(): void { 58 | this._hitsPerSecond++ 59 | this._hitsPerMinute++ 60 | this._concurrentRequests++ 61 | this._allRequests++ 62 | } 63 | 64 | public doneRequest(): void { 65 | this._concurrentRequests-- 66 | } 67 | 68 | private toMegabytes(bytes: number): string { 69 | return (bytes / 1048576).toFixed(2) 70 | } 71 | } -------------------------------------------------------------------------------- /src/modules/utils/BaseConsoleApplication.ts: -------------------------------------------------------------------------------- 1 | import {writeFile, readFile, readdir} from "fs"; 2 | 3 | export default class BaseConsoleApplication { 4 | protected env = process.env['NODE_ENV'] || 'development' 5 | protected isProduction = this.env == 'production' 6 | 7 | protected die(error: string) { 8 | console.error(error) 9 | process.exit() 10 | } 11 | 12 | protected getArgs() { 13 | let result: any = { } 14 | 15 | process.argv.forEach(arg => { 16 | let test = arg.split('=') 17 | 18 | if (test[0] && test[1] && (test[0][0] + test[0][0]) == '--') { 19 | result[test[0].replace('--', '')] = test[1] 20 | } 21 | }) 22 | 23 | return result 24 | } 25 | 26 | protected async saveToFile(name: string, data: string): Promise { 27 | return new Promise((resolve, reject) => { 28 | writeFile(name, data, err => { 29 | if (err) { 30 | reject(err) 31 | return 32 | } 33 | 34 | resolve() 35 | }) 36 | }) 37 | } 38 | 39 | protected async readFile(name: string): Promise { 40 | return new Promise((resolve, reject) => { 41 | readFile(name, 'utf-8', (err, data) => { 42 | if (err) { 43 | reject(err) 44 | return 45 | } 46 | 47 | resolve(data) 48 | }) 49 | }) 50 | } 51 | 52 | protected async readDir(dir: string): Promise { 53 | return new Promise((resolve, reject) => { 54 | readdir(dir, (err, files) => { 55 | if (err) { 56 | reject(err) 57 | return 58 | } 59 | 60 | resolve(files) 61 | }) 62 | }) 63 | } 64 | } -------------------------------------------------------------------------------- /src/modules/utils/CallbackQueue.ts: -------------------------------------------------------------------------------- 1 | export default class CallbackQueue { 2 | private _delay: number 3 | private _queue: Function[] = [] 4 | 5 | constructor(countPerSec: number) { 6 | this._delay = 1000 / countPerSec 7 | 8 | setInterval(() => { 9 | if(this._queue.length !== 0){ 10 | const func = this._queue.shift() 11 | if (func) //just to make ts happy 12 | func() 13 | } 14 | }, this._delay) 15 | } 16 | 17 | public push(func: Function) { 18 | this._queue.push(func) 19 | } 20 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "build/", 5 | "sourceMap": true, 6 | "target": "es6", 7 | "module": "commonjs", 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "allowJs": false, 11 | "strict": true, 12 | "declaration": true, 13 | "pretty": true 14 | }, 15 | "include": [ 16 | "src/**/*" 17 | ], 18 | "exclude": [ 19 | "node_modules", 20 | "**/*.spec.ts" 21 | ] 22 | } --------------------------------------------------------------------------------