├── .editorconfig ├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json └── tasks.json ├── LICENSE ├── Readme.md ├── package-lock.json ├── package.json ├── src ├── application.ts ├── bus │ ├── busAdapter.ts │ ├── localAdapter.ts │ ├── messageBus.ts │ └── rabbitAdapter.ts ├── commands │ ├── abstractCommand.ts │ ├── abstractHttpCommand.ts │ ├── abstractProviderCommand.ts │ ├── abstractServiceCommand.ts │ ├── circuitBreaker.ts │ ├── command.ts │ ├── commandFactory.ts │ ├── commandProperties.ts │ ├── executionResult.ts │ ├── http │ │ └── hystrixSSEStream.ts │ ├── metrics │ │ ├── commandMetricsFactory.ts │ │ └── hystrix │ │ │ ├── counterBucket.ts │ │ │ ├── hystrixCommandMetrics.ts │ │ │ ├── percentileBucket.ts │ │ │ ├── rollingNumber.ts │ │ │ ├── rollingNumberEvent.ts │ │ │ └── rollingPercentile.ts │ ├── semaphore.ts │ └── types.ts ├── configurations │ ├── abstractions.ts │ ├── configurationManager.ts │ ├── configurationSourceBuilder.ts │ ├── dynamicConfiguration.ts │ ├── properties │ │ ├── chainedPropertyValue.ts │ │ └── dynamicProperty.ts │ └── sources │ │ ├── PrioritizedSourceValue.ts │ │ ├── abstractRemoteSource.ts │ │ ├── consulConfigurationSource.ts │ │ ├── environmentVariableSource.ts │ │ ├── fileConfigurationSource.ts │ │ ├── httpConfigurationSource.ts │ │ ├── memoryConfigurationSource.ts │ │ └── vulcainConfigurationSource.ts ├── defaults │ ├── crudHandlers.ts │ ├── dependencyExplorer.ts │ ├── scopeDescriptors.ts │ ├── serviceExplorer.ts │ └── swagger │ │ ├── _swaggerTemplate.ts │ │ ├── swaggerApiDefinition.ts │ │ ├── swaggerServiceDescriptions.ts │ │ └── swaggerUIHandler.ts ├── di │ ├── annotations.ts │ ├── containers.ts │ ├── resolvers.ts │ ├── scope.ts │ └── serviceResolver.ts ├── globals │ ├── manifest.ts │ ├── settings.ts │ └── system.ts ├── graphql │ ├── directives.ts │ ├── graphQLAdapter.ts │ ├── graphQLHandler.ts │ └── typeBuilder.ts ├── index.ts ├── instrumentations │ ├── common.ts │ ├── metrics │ │ ├── applicationInsightsMetrics.ts │ │ ├── consoleMetrics.ts │ │ ├── index.ts │ │ ├── prometheusMetrics.ts │ │ └── statsdMetrics.ts │ ├── span.ts │ └── trackers │ │ ├── JaegerInstrumentation.ts │ │ ├── index.ts │ │ └── zipkinInstrumentation.ts ├── log │ ├── logger.ts │ └── vulcainLogger.ts ├── pipeline │ ├── common.ts │ ├── errors │ │ ├── applicationRequestError.ts │ │ ├── badRequestError.ts │ │ ├── commandRuntimeError.ts │ │ ├── runtimeError.ts │ │ └── timeoutError.ts │ ├── handlerProcessor.ts │ ├── handlers │ │ ├── abstractHandlers.ts │ │ ├── action │ │ │ ├── actionManager.ts │ │ │ ├── annotations.ts │ │ │ ├── definitions.ts │ │ │ └── eventHandlerFactory.ts │ │ ├── definitions.ts │ │ ├── descriptions │ │ │ ├── operationDescription.ts │ │ │ ├── propertyDescription.ts │ │ │ ├── schemaDescription.ts │ │ │ ├── serviceDescription.ts │ │ │ └── serviceDescriptions.ts │ │ ├── errorResponse.ts │ │ ├── query │ │ │ ├── annotations.query.ts │ │ │ ├── annotations.queryHandler.ts │ │ │ ├── definitions.ts │ │ │ ├── queryManager.ts │ │ │ └── queryResult.ts │ │ └── utils.ts │ ├── middlewares │ │ ├── ServerSideEventMiddleware.ts │ │ ├── authenticationMiddleware.ts │ │ ├── handlersMiddleware.ts │ │ └── normalizeDataMiddleware.ts │ ├── policies │ │ └── defaultTenantPolicy.ts │ ├── requestContext.ts │ ├── response.ts │ ├── serializers │ │ ├── defaultSerializer.ts │ │ └── serializer.ts │ ├── serverAdapter.ts │ ├── testContext.ts │ ├── vulcainPipeline.ts │ └── vulcainServer.ts ├── preloader.ts ├── providers │ ├── memory │ │ ├── mongoQueryParser.ts │ │ ├── provider.ts │ │ └── providerFactory.ts │ ├── mongo │ │ └── provider.ts │ ├── provider.ts │ └── taskManager.ts ├── schemas │ ├── builder │ │ ├── annotations.model.ts │ │ ├── annotations.property.ts │ │ ├── annotations.ts │ │ └── schemaBuilder.ts │ ├── domain.ts │ ├── schema.ts │ ├── schemaInfo.ts │ ├── schemaType.ts │ ├── standards │ │ ├── alphanumeric.ts │ │ ├── arrayOf.ts │ │ ├── boolean.ts │ │ ├── date-iso8601.ts │ │ ├── email.ts │ │ ├── enum.ts │ │ ├── id.ts │ │ ├── integer.ts │ │ ├── length.ts │ │ ├── number.ts │ │ ├── pattern.ts │ │ ├── range.ts │ │ ├── reference.ts │ │ ├── standards.ts │ │ ├── string.ts │ │ ├── uid.ts │ │ └── url.ts │ ├── validator.ts │ └── visitor.ts ├── security │ ├── authorizationPolicy.ts │ ├── securityContext.ts │ └── services │ │ └── tokenService.ts ├── stubs │ ├── istubManager.ts │ └── stubManager.ts └── utils │ ├── actualTime.ts │ ├── conventions.ts │ ├── crypto.ts │ ├── files.ts │ └── reflector.ts ├── test ├── command │ ├── circuitBreaker.spec.ts │ ├── command.spec.ts │ └── commands.ts ├── configurations │ ├── crypto.spec.ts │ ├── index.spec.ts │ └── system.spec.ts ├── defaultHandlers │ ├── defaultHandlers.spec.ts │ └── overrideHandlers.spec.ts ├── di │ └── container.spec.ts ├── http │ └── hystrixSSEStream.spec.ts ├── metrics │ ├── commandMetrics.spec.ts │ ├── counterBucket.spec.ts │ ├── percentileBucket.spec.ts │ ├── rollingNumber.spec.ts │ └── rollingPercentile.spec.ts ├── mocks │ └── mockService.spec.ts ├── providers │ └── memory.spec.ts └── schema │ ├── bindData.spec.ts │ ├── sensibleData.spec.ts │ └── validateData.spec.ts ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | .DS_Store 4 | data/ 5 | coverage/ 6 | npm-debug.log 7 | *.tgz 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | before_deploy: 5 | deploy: 6 | provider: npm 7 | api_key: $NPM_KEY 8 | email: "alain.metge@zenasoft.com" 9 | on: 10 | branch: master 11 | tag: next 12 | branches: 13 | only: 14 | - master -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Test with debug", 6 | "type": "node", 7 | "request": "launch", 8 | "smartStep": true, 9 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 10 | "stopOnEntry": false, 11 | "preLaunchTask": "build", 12 | "args": ["${workspaceRoot}/lib/test/**/*.spec.js"], 13 | "cwd": "${workspaceRoot}", 14 | "env": { 15 | "VULCAIN_ENV": "test", 16 | "VULCAIN_SERVICE_NAME": "core", 17 | "VULCAIN_SERVICE_VERSION": "1.0", 18 | "VULCAIN_DOMAIN": "vulcain", 19 | "NODE_ENV": "development" 20 | }, 21 | "console": "internalConsole", 22 | "sourceMaps": true, 23 | "outFiles": ["${workspaceRoot}/lib/**/*.js"] 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "test", 9 | "group": { 10 | "kind": "test", 11 | "isDefault": true 12 | }, 13 | "problemMatcher": "$tsc" 14 | }, 15 | { 16 | "identifier": "build", 17 | "type": "npm", 18 | "script": "build", 19 | "group": { 20 | "kind": "build", 21 | "isDefault": true 22 | }, 23 | "problemMatcher":"$tsc" 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Micro service framework 2 | 3 | ![npm version](https://img.shields.io/npm/v/vulcain-corejs.svg) 4 | [![Build Status](https://travis-ci.org/vulcainjs/vulcain-corejs.svg?branch=master)](https://travis-ci.org/vulcainjs/vulcain-corejs) 5 | [![license](https://img.shields.io/npm/l/vulcain-corejs.svg)](https://www.npmjs.com/package/vulcain-corejs) 6 | ![Downloads](https://img.shields.io/npm/dm/vulcain-corejs.svg) 7 | 8 | A backend micro-service framework for nodejs writing in typescript. 9 | 10 | See [web site](http://www.vulcainjs.org) for more details or try [the samples](https://github.com/vulcainjs/vulcain-samples) 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vulcain-corejs", 3 | "version": "2.0.0-beta954", 4 | "description": "Vulcain micro-service framework", 5 | "main": "lib/src/index.js", 6 | "scripts": { 7 | "test": "npm run build && mocha --reporter spec ./lib/test/**/*.spec", 8 | "build": "tsc -p ." 9 | }, 10 | "files": [ 11 | "lib/src" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/vulcainjs/vulcain-corejs.git" 16 | }, 17 | "maintainers": [ 18 | "Alain Metge" 19 | ], 20 | "engines": { 21 | "node": ">=6.0" 22 | }, 23 | "license": "Apache-2.0", 24 | "typings": "lib/src/index.d.ts", 25 | "devDependencies": { 26 | "@types/amqplib": "^0.5.7", 27 | "@types/chai": "^4.1.0", 28 | "@types/fast-stats": "0.0.29", 29 | "@types/jsonwebtoken": "^7.2.5", 30 | "@types/mocha": "^5.0.0", 31 | "@types/mongodb": "^3.0.9", 32 | "chai": "^4.1.2", 33 | "mocha": "^5.0.4", 34 | "tslint": "^5.9.1", 35 | "typescript": "^2.7.2" 36 | }, 37 | "dependencies": { 38 | "amqplib": "^0.5.2", 39 | "fast-stats": "0.0.3", 40 | "graphql": "^0.13.1", 41 | "jaeger-client": "^3.10.0", 42 | "jsonwebtoken": "^8.2.0", 43 | "moment": "^2.21.0", 44 | "mongodb": "^3.0.4", 45 | "prom-client": "^11.0.0", 46 | "reflect-metadata": "^0.1.3", 47 | "router": "^1.3.2", 48 | "rxjs": "^5.5.7", 49 | "swagger-ui-dist": "^3.12.1", 50 | "unirest": "^0.5.0", 51 | "uuid": "^3.0.1", 52 | "validator": "^9.4.1", 53 | "zipkin": "^0.12.0", 54 | "zipkin-transport-http": "^0.12.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/bus/busAdapter.ts: -------------------------------------------------------------------------------- 1 | import { RequestData } from "../pipeline/common"; 2 | import { EventData } from "./messageBus"; 3 | 4 | /** 5 | * Async actions dispatcher 6 | * Connect only instances of a same service 7 | * 8 | * @export 9 | * @interface IActionBusAdapter 10 | */ 11 | export interface IActionBusAdapter { 12 | /** 13 | * Open bus 14 | * 15 | */ 16 | open(): Promise; 17 | 18 | /** 19 | * Gracefully stop message consumption 20 | */ 21 | stopReception(); 22 | 23 | /** 24 | * Publish an async action 25 | * 26 | * @param {string} domain 27 | * @param {string} serviceId 28 | * @param {RequestData} command 29 | */ 30 | publishTask(domain: string, serviceId: string, command: RequestData); 31 | /** 32 | * Consume an async action 33 | * 34 | * @param {string} domain 35 | * @param {string} serviceId 36 | * @param {Function} handler 37 | * 38 | * @memberOf IActionBusAdapter 39 | */ 40 | consumeTask(domain: string, serviceId: string, handler: (event: RequestData) => void); 41 | } 42 | 43 | /** 44 | * Global event bus 45 | * Events are shared by all service instance. 46 | * Event is sent when action complete 47 | * 48 | * @export 49 | * @interface IEventBusAdapter 50 | */ 51 | export interface IEventBusAdapter { 52 | /** 53 | * Open bus 54 | */ 55 | open(); 56 | /** 57 | * Gracefully stop message consumption 58 | */ 59 | stopReception(); 60 | /** 61 | * send event 62 | */ 63 | sendEvent(domain: string, event: EventData); 64 | /** 65 | * Consume events 66 | */ 67 | consumeEvents(domain: string, handler: (event: EventData) => void, distributionKey?:string); 68 | } -------------------------------------------------------------------------------- /src/bus/localAdapter.ts: -------------------------------------------------------------------------------- 1 | import { IActionBusAdapter, IEventBusAdapter } from '../bus/busAdapter'; 2 | import { EventData } from "./messageBus"; 3 | import { RequestData } from "../pipeline/common"; 4 | import * as RX from 'rxjs'; 5 | 6 | export 7 | class LocalAdapter implements IActionBusAdapter, IEventBusAdapter { 8 | private eventQueue: RX.Subject; 9 | private taskQueue: RX.Subject; 10 | 11 | open() { 12 | this.eventQueue = new RX.Subject(); 13 | this.taskQueue = new RX.Subject(); 14 | return Promise.resolve(); 15 | } 16 | 17 | stopReception() { 18 | this.eventQueue = null; 19 | this.taskQueue = null; 20 | } 21 | 22 | sendEvent(domain: string, event: EventData) { 23 | this.eventQueue && this.eventQueue.next(event); 24 | } 25 | 26 | consumeEvents(domain: string, handler: (event: EventData) => void, queueName?: string) { 27 | this.eventQueue && this.eventQueue.subscribe(handler); 28 | } 29 | 30 | publishTask(domain: string, serviceId: string, command: RequestData) { 31 | this.taskQueue && this.taskQueue.next(command); 32 | } 33 | 34 | consumeTask(domain: string, serviceId: string, handler: (event: RequestData) => void) { 35 | this.taskQueue && this.taskQueue.subscribe(handler); 36 | } 37 | } -------------------------------------------------------------------------------- /src/commands/abstractCommand.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionResult } from './executionResult'; 2 | import { DefaultServiceNames } from '../di/annotations'; 3 | import { IContainer, IInjectionNotification } from '../di/resolvers'; 4 | import { Inject } from '../di/annotations'; 5 | import { IMetrics } from '../instrumentations/metrics'; 6 | import { VulcainLogger } from '../log/vulcainLogger'; 7 | import { IRequestContext } from "../pipeline/common"; 8 | import { Span } from '../instrumentations/span'; 9 | import { ISpanTracker } from '../instrumentations/common'; 10 | 11 | /** 12 | * command 13 | * 14 | * @export 15 | * @interface ICommand 16 | */ 17 | export interface ICommand { 18 | context: IRequestContext; 19 | } 20 | 21 | /** 22 | * 23 | * 24 | * @export 25 | * @abstract 26 | * @class AbstractCommand 27 | * @template T 28 | */ 29 | export abstract class AbstractCommand { 30 | 31 | /** 32 | * 33 | * 34 | * @type {RequestContext} 35 | */ 36 | public context: IRequestContext; 37 | 38 | /** 39 | * Components container 40 | * 41 | * @readonly 42 | * 43 | * @memberOf AbstractCommand 44 | */ 45 | @Inject(DefaultServiceNames.Container) 46 | container: IContainer; 47 | 48 | /** 49 | * Creates an instance of AbstractCommand. 50 | * 51 | */ 52 | constructor(context: IRequestContext) { 53 | this.context = context; 54 | } 55 | 56 | protected setMetricsTags(command: string, tags: { [key: string]: string }) { 57 | let tracker = this.context.requestTracker; 58 | tracker.trackAction(command); 59 | 60 | Object.keys(tags) 61 | .forEach(key => 62 | tracker.addTag(key, tags[key])); 63 | } 64 | 65 | // Must be defined in command 66 | // protected fallback(err, ...args) 67 | } 68 | -------------------------------------------------------------------------------- /src/commands/abstractProviderCommand.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from '../schemas/schema'; 2 | import { IProvider } from '../providers/provider'; 3 | import { DefaultServiceNames } from '../di/annotations'; 4 | import { IContainer } from '../di/resolvers'; 5 | import { Domain } from '../schemas/domain'; 6 | import { Inject } from '../di/annotations'; 7 | import { IMetrics } from '../instrumentations/metrics'; 8 | import { IProviderFactory } from '../providers/provider'; 9 | import { Service } from '../globals/system'; 10 | import { VulcainLogger } from '../log/vulcainLogger'; 11 | import { IRequestContext } from "../pipeline/common"; 12 | import { Span } from '../instrumentations/span'; 13 | import { ISpanTracker } from '../instrumentations/common'; 14 | 15 | /** 16 | * 17 | * 18 | * @export 19 | * @abstract 20 | * @class AbstractCommand 21 | * @template T schema 22 | */ 23 | export abstract class AbstractProviderCommand { 24 | 25 | protected providerFactory: IProviderFactory; 26 | 27 | public context: IRequestContext; 28 | 29 | /** 30 | * 31 | * 32 | * @type {IProvider} 33 | */ 34 | provider: IProvider; 35 | /** 36 | * 37 | * 38 | * @type {Schema} 39 | */ 40 | schema: Schema; 41 | 42 | public container: IContainer; 43 | 44 | /** 45 | * Creates an instance of AbstractCommand. 46 | * 47 | * @param {IContainer} container 48 | * @param {any} providerFactory 49 | */ 50 | constructor(context: IRequestContext) { 51 | this.context = context; 52 | this.container = context.container; 53 | this.providerFactory = this.container.get(DefaultServiceNames.ProviderFactory); 54 | } 55 | 56 | /** 57 | * 58 | * 59 | * @param {string} schema 60 | */ 61 | setSchema(schema: string): string { 62 | if (schema && !this.provider) { 63 | this.schema = this.container.get(DefaultServiceNames.Domain).getSchema(schema); 64 | this.provider = this.providerFactory.getConnection(this.context, this.context.user.tenant); 65 | return this.schema.name; 66 | } 67 | } 68 | 69 | protected setMetricTags(verb: string, address: string, schema: string, tenant?: string) { 70 | Service.manifest.registerProvider(address, schema); 71 | let tracker = this.context.requestTracker; 72 | tracker.trackAction(verb); 73 | tracker.addProviderCommandTags(address, schema, (tenant || this.context.user.tenant)); 74 | } 75 | 76 | // Must be defined in command 77 | // protected fallback(err, ...args) 78 | } 79 | -------------------------------------------------------------------------------- /src/commands/circuitBreaker.ts: -------------------------------------------------------------------------------- 1 | import ActualTime from "../utils/actualTime"; 2 | import { CommandProperties } from "./commandProperties"; 3 | import { CommandMetricsFactory } from "./metrics/commandMetricsFactory"; 4 | import { HystrixCommandMetrics } from "./metrics/hystrix/hystrixCommandMetrics"; 5 | 6 | export interface CircuitBreaker { 7 | allowRequest(): boolean; 8 | markSuccess(): void; 9 | isOpen(): boolean; 10 | properties: CommandProperties; 11 | } 12 | 13 | class NoOpCircuitBreaker implements CircuitBreaker { 14 | constructor(private commandKey: string, public properties: CommandProperties) { } 15 | 16 | allowRequest() { return true; } 17 | markSuccess() { } 18 | isOpen() { return false; } 19 | } 20 | 21 | class DefaultCircuitBreaker implements CircuitBreaker { 22 | private circuitOpen: boolean; 23 | private circuitOpenedOrLastTestedTime; 24 | 25 | constructor(private commandKey: string, public properties: CommandProperties) { 26 | this.circuitOpen = false; 27 | this.circuitOpenedOrLastTestedTime = ActualTime.getCurrentTime(); 28 | } 29 | 30 | allowRequest() { 31 | if (this.properties.circuitBreakerForceOpen.value) { 32 | return false; 33 | } 34 | 35 | if (this.properties.circuitBreakerForceClosed.value) { 36 | // this.isOpen(); 37 | return true; 38 | } 39 | return !this.isOpen() || this.allowSingleTest(); 40 | } 41 | 42 | get metrics() { 43 | return CommandMetricsFactory.getOrCreate(this.properties); 44 | } 45 | 46 | allowSingleTest() { 47 | if (this.circuitOpen && ActualTime.getCurrentTime() > this.circuitOpenedOrLastTestedTime + this.properties.circuitBreakerSleepWindowInMilliseconds.value) { 48 | this.circuitOpenedOrLastTestedTime = ActualTime.getCurrentTime(); 49 | return true; 50 | } else { 51 | return false; 52 | } 53 | } 54 | 55 | isOpen() { 56 | if (this.circuitOpen) { 57 | return true; 58 | } 59 | 60 | let {totalCount = 0, errorCount, errorPercentage} = (this.metrics).getHealthCounts(); 61 | if (totalCount < this.properties.circuitBreakerRequestVolumeThreshold.value) { 62 | return false; 63 | } 64 | 65 | if (errorPercentage > this.properties.circuitBreakerErrorThresholdPercentage.value) { 66 | this.circuitOpen = true; 67 | this.circuitOpenedOrLastTestedTime = ActualTime.getCurrentTime(); 68 | return true; 69 | } else { 70 | return false; 71 | } 72 | } 73 | 74 | markSuccess() { 75 | if (this.circuitOpen) { 76 | this.circuitOpen = false; 77 | (this.metrics).reset(); 78 | } 79 | } 80 | } 81 | 82 | const circuitBreakersByCommand = new Map(); 83 | 84 | export class CircuitBreakerFactory { 85 | 86 | static getOrCreate(properties: CommandProperties) { 87 | 88 | let previouslyCached = circuitBreakersByCommand.get(properties.commandName); 89 | if (previouslyCached) { 90 | return previouslyCached; 91 | } 92 | 93 | let circuitBreaker = properties.circuitBreakerEnabled ? 94 | new DefaultCircuitBreaker(properties.commandName, properties) : 95 | new NoOpCircuitBreaker(properties.commandName, properties); 96 | 97 | circuitBreakersByCommand.set(properties.commandName, circuitBreaker); 98 | return circuitBreakersByCommand.get(properties.commandName); 99 | 100 | } 101 | 102 | static get(commandName: string): CircuitBreaker { 103 | return circuitBreakersByCommand.get(commandName); 104 | } 105 | 106 | static getCache() { 107 | return circuitBreakersByCommand; 108 | } 109 | 110 | static resetCache() { 111 | circuitBreakersByCommand.clear(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/commands/executionResult.ts: -------------------------------------------------------------------------------- 1 | 2 | export enum EventType { 3 | SUCCESS, 4 | SHORT_CIRCUITED, 5 | SEMAPHORE_REJECTED, 6 | TIMEOUT, 7 | FAILURE, 8 | FALLBACK_REJECTION, 9 | FALLBACK_SUCCESS, 10 | FALLBACK_FAILURE, 11 | RESPONSE_FROM_CACHE 12 | } 13 | 14 | export enum FailureType { 15 | SHORTCIRCUIT, 16 | REJECTED_SEMAPHORE_EXECUTION, 17 | TIMEOUT, 18 | COMMAND_EXCEPTION, 19 | REJECTED_SEMAPHORE_FALLBACK 20 | } 21 | 22 | export class ExecutionResult { 23 | public events: Array = new Array(); 24 | 25 | /** 26 | * Whether the response was returned successfully either by executing run() or from cache. 27 | * 28 | * @return bool 29 | */ 30 | get isSuccessfulExecution() { 31 | return this.eventExists(EventType.SUCCESS); 32 | } 33 | 34 | /** 35 | * Whether the run() resulted in a failure (exception). 36 | * 37 | * @return bool 38 | */ 39 | get isFailedExecution() { 40 | return this.eventExists(EventType.FAILURE); 41 | } 42 | 43 | 44 | /** 45 | * Get the Throwable/Exception thrown that caused the failure. 46 | *

47 | * If IsFailedExecution { get == true then this would represent the Exception thrown by the run() method. 48 | *

49 | * If IsFailedExecution { get == false then this would return null. 50 | * 51 | * @return Throwable or null 52 | */ 53 | public failedExecutionException; 54 | 55 | /** 56 | * Whether the response received from was the result of some type of failure 57 | * and Fallback { get being called. 58 | * 59 | * @return bool 60 | */ 61 | get isResponseFromFallback() { 62 | return this.eventExists(EventType.FALLBACK_SUCCESS); 63 | } 64 | 65 | /** 66 | * Whether the response received was the result of a timeout 67 | * and Fallback { get being called. 68 | * 69 | * @return bool 70 | */ 71 | get isResponseTimedOut() { 72 | return this.eventExists(EventType.TIMEOUT); 73 | } 74 | 75 | /** 76 | * Whether the response received was a fallback as result of being 77 | * short-circuited (meaning IsCircuitBreakerOpen { get == true) and Fallback { get being called. 78 | * 79 | * @return bool 80 | */ 81 | get isResponseShortCircuited() { 82 | return this.eventExists(EventType.SHORT_CIRCUITED); 83 | } 84 | 85 | /** 86 | * Whether the response is from cache and run() was not invoked. 87 | * 88 | * @return bool 89 | */ 90 | get isResponseFromCache() { 91 | return this.eventExists(EventType.RESPONSE_FROM_CACHE); 92 | } 93 | 94 | /** 95 | * Whether the response received was a fallback as result of being 96 | * rejected (from thread-pool or semaphore) and Fallback { get being called. 97 | * 98 | * @return bool 99 | */ 100 | get isResponseRejected() { 101 | return this.eventExists(EventType.SEMAPHORE_REJECTED); 102 | } 103 | 104 | /** 105 | * List of CommandEventType enums representing events that occurred during execution. 106 | *

107 | * Examples of events are SUCCESS, FAILURE, TIMEOUT, and SHORT_CIRCUITED 108 | * 109 | * @return {@code List} 110 | */ 111 | get executionEvents() { 112 | return this.events; 113 | } 114 | 115 | /** 116 | * The execution time of this command instance in milliseconds, or -1 if not executed. 117 | * 118 | * @return int 119 | */ 120 | public executionTime: number; 121 | 122 | /** 123 | * If this command has completed execution either successfully, via fallback or failure. 124 | * 125 | */ 126 | isExecutionComplete: boolean = false; 127 | 128 | addEvent(evt: EventType) { 129 | this.events.push(evt); 130 | } 131 | 132 | protected eventExists(evt: EventType) { 133 | return this.events.indexOf(evt) >= 0; 134 | } 135 | } -------------------------------------------------------------------------------- /src/commands/metrics/commandMetricsFactory.ts: -------------------------------------------------------------------------------- 1 | import { HystrixCommandMetrics } from './hystrix/hystrixCommandMetrics'; 2 | import { CommandProperties } from "../commandProperties"; 3 | 4 | export interface ICommandMetrics { 5 | incrementExecutionCount(); 6 | markTimeout(); 7 | markSuccess(); 8 | addExecutionTime(duration: number); 9 | markRejected(); 10 | markShortCircuited(); 11 | decrementExecutionCount(); 12 | markFallbackSuccess(); 13 | markFallbackFailure(); 14 | markFallbackRejection(); 15 | markExceptionThrown(); 16 | markFailure(); 17 | markBadRequest(duration: number); 18 | } 19 | 20 | export class CommandMetricsFactory { 21 | private static metricsByCommand = new Map(); 22 | 23 | static getOrCreate(options:CommandProperties): ICommandMetrics { 24 | let previouslyCached = CommandMetricsFactory.metricsByCommand.get(options.commandName); 25 | if (previouslyCached) { 26 | return previouslyCached; 27 | } 28 | 29 | let metrics = new HystrixCommandMetrics(options); 30 | 31 | CommandMetricsFactory.metricsByCommand.set(options.commandName, metrics); 32 | return metrics; 33 | 34 | } 35 | 36 | static get(commandName:string): ICommandMetrics { 37 | return CommandMetricsFactory.metricsByCommand.get(commandName); 38 | } 39 | 40 | static resetCache() { 41 | CommandMetricsFactory.metricsByCommand.clear(); 42 | } 43 | 44 | static getAllMetrics() { 45 | return CommandMetricsFactory.metricsByCommand.values(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/commands/metrics/hystrix/counterBucket.ts: -------------------------------------------------------------------------------- 1 | import RollingNumberEvent from "./rollingNumberEvent"; 2 | 3 | export default class CounterBucket { 4 | private bucketValues; 5 | public windowStart: number; 6 | 7 | constructor () { 8 | this.bucketValues = {}; 9 | } 10 | 11 | reset(windowsStart: number) { 12 | this.windowStart = windowsStart; 13 | this.bucketValues = {}; 14 | } 15 | 16 | get(type) { 17 | if (RollingNumberEvent[type] === undefined) { 18 | throw new Error("invalid event"); 19 | } 20 | 21 | if (!this.bucketValues[type]) { 22 | return 0; 23 | } 24 | return this.bucketValues[type]; 25 | } 26 | 27 | increment(type) { 28 | if (RollingNumberEvent[type] === undefined) { 29 | throw new Error("invalid event"); 30 | } 31 | 32 | let value = this.bucketValues[type]; 33 | if (value) { 34 | value = value + 1; 35 | this.bucketValues[type] = value; 36 | } else { 37 | this.bucketValues[type] = 1; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/commands/metrics/hystrix/percentileBucket.ts: -------------------------------------------------------------------------------- 1 | 2 | export default class PercentileBucket { 3 | private bucketValues: Array; 4 | private pos: number = 0; 5 | public length: number = 0; 6 | public windowStart: number; 7 | 8 | constructor( private dataLength=100) { 9 | this.bucketValues = []; 10 | } 11 | 12 | addValue(value: number) { 13 | this.bucketValues[this.pos] = value; 14 | this.pos = (this.pos + 1) % this.dataLength; // roll over 15 | if( this.length < this.dataLength) 16 | this.length = this.length + 1; 17 | } 18 | 19 | get values() { 20 | if (this.length === 0) 21 | return null; 22 | return this.length < this.dataLength ? this.bucketValues.slice(0, this.length) : this.bucketValues; 23 | } 24 | 25 | reset(windowStart: number) { 26 | this.windowStart = windowStart; 27 | this.pos = this.length = 0; 28 | this.bucketValues = []; 29 | } 30 | } -------------------------------------------------------------------------------- /src/commands/metrics/hystrix/rollingNumber.ts: -------------------------------------------------------------------------------- 1 | import Bucket from "./counterBucket"; 2 | import ActualTime from "../../../utils/actualTime"; 3 | 4 | export class RollingNumber { 5 | private windowLength: number; 6 | private numberOfBuckets: number; 7 | private buckets; 8 | private currentBucketIndex = 0; 9 | private bucketsLength: number; 10 | public bucketSizeInMilliseconds:number; 11 | 12 | constructor(timeInMillisecond: number, numberOfBuckets: number) { 13 | if (timeInMillisecond <= 0 || numberOfBuckets <= 0) 14 | throw new Error("Invalid arguments for RollingNumber. Must be > 0."); 15 | 16 | this.windowLength = timeInMillisecond; 17 | this.numberOfBuckets = numberOfBuckets; 18 | this.bucketSizeInMilliseconds = this.windowLength / this.numberOfBuckets; 19 | 20 | this.buckets = []; 21 | // Pre initialize buckets 22 | for (let i = 0; i < numberOfBuckets; i++) { 23 | this.buckets[i] = new Bucket(); 24 | } 25 | this.buckets[0].reset(ActualTime.getCurrentTime()); 26 | this.bucketsLength = 1; 27 | } 28 | 29 | get length() { 30 | return this.bucketsLength; 31 | } 32 | 33 | increment(type) { 34 | this.getCurrentBucket().increment(type); 35 | } 36 | 37 | getCurrentBucket() { 38 | let currentTime = ActualTime.getCurrentTime(); 39 | let currentBucket = this.buckets[this.currentBucketIndex]; 40 | if (currentTime < (currentBucket.windowStart + this.bucketSizeInMilliseconds)) { 41 | return currentBucket; 42 | } 43 | 44 | this.currentBucketIndex = (this.currentBucketIndex + 1) % this.numberOfBuckets; 45 | currentBucket = this.buckets[this.currentBucketIndex]; 46 | currentBucket.reset(currentTime); 47 | if (this.bucketsLength < this.numberOfBuckets) 48 | this.bucketsLength++; 49 | 50 | return currentBucket; 51 | } 52 | 53 | getRollingSum(type) { 54 | this.getCurrentBucket(); 55 | let startingWindowTime = ActualTime.getCurrentTime() - this.windowLength; 56 | let sum = 0; 57 | for (let i = 0; i < this.bucketsLength; i++) { 58 | if( this.buckets[i].windowStart >= startingWindowTime) 59 | sum += this.buckets[i].get(type); 60 | } 61 | return sum; 62 | } 63 | 64 | reset() { 65 | for (let i = 0; i < this.bucketsLength; i++) { 66 | this.buckets[i].reset(); 67 | } 68 | this.currentBucketIndex = 0; 69 | this.bucketsLength = 1; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/commands/metrics/hystrix/rollingNumberEvent.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | SUCCESS: "SUCCESS", 3 | FAILURE: "FAILURE", 4 | TIMEOUT: "TIMEOUT", 5 | REJECTED: "REJECTED", 6 | SHORT_CIRCUITED: "SHORT_CIRCUITED", 7 | FALLBACK_SUCCESS: "FALLBACK_SUCCESS", 8 | FALLBACK_FAILURE: "FALLBACK_FAILURE", 9 | FALLBACK_REJECTION: "FALLBACK_REJECTION", 10 | EXCEPTION_THROWN: "EXCEPTION_THROWN", 11 | BAD_REQUEST: "BAD_REQUEST", 12 | RESPONSE_FROM_CACHE: "RESPONSE_FROM_CACHE" 13 | }; 14 | -------------------------------------------------------------------------------- /src/commands/metrics/hystrix/rollingPercentile.ts: -------------------------------------------------------------------------------- 1 | import Bucket from "./percentileBucket"; 2 | import {Stats} from 'fast-stats'; 3 | import ActualTime from "../../../utils/actualTime"; 4 | 5 | export class RollingPercentile { 6 | 7 | private windowLength: number; 8 | private numberOfBuckets: number; 9 | private buckets; 10 | private percentileSnapshot: PercentileSnapshot; 11 | private currentBucketIndex = 0; 12 | 13 | constructor(timeInMillisecond: number, numberOfBuckets: number) { 14 | if (timeInMillisecond <= 0 || numberOfBuckets <= 0) 15 | throw new Error("Invalid arguments for RollingPercentile. Must be > 0."); 16 | this.windowLength = timeInMillisecond; 17 | this.numberOfBuckets = numberOfBuckets; 18 | this.buckets = []; 19 | this.percentileSnapshot = new PercentileSnapshot(); 20 | 21 | // Pre initialize buckets 22 | for (let i = 0; i < numberOfBuckets; i++) { 23 | this.buckets[i] = new Bucket(); 24 | } 25 | this.buckets[0].windowStart = ActualTime.getCurrentTime(); 26 | } 27 | 28 | get bucketSizeInMilliseconds() { 29 | return this.windowLength / this.numberOfBuckets; 30 | } 31 | 32 | addValue(value) { 33 | this.getCurrentBucket().addValue(value); 34 | } 35 | 36 | getPercentile(percentile) { 37 | this.getCurrentBucket(); 38 | return this.percentileSnapshot.getPercentile(percentile); 39 | } 40 | 41 | getCurrentBucket() { 42 | let currentTime = ActualTime.getCurrentTime(); 43 | let currentBucket = this.buckets[this.currentBucketIndex]; 44 | if (currentTime < (currentBucket.windowStart + this.bucketSizeInMilliseconds)) { 45 | return currentBucket; 46 | } 47 | 48 | this.currentBucketIndex = (this.currentBucketIndex + 1) % this.numberOfBuckets; 49 | currentBucket = this.buckets[this.currentBucketIndex]; 50 | currentBucket.reset(currentTime); 51 | this.percentileSnapshot = new PercentileSnapshot(this.buckets); 52 | return currentBucket; 53 | } 54 | 55 | getLength() { 56 | let length=0; 57 | for (let bucket of this.buckets) { 58 | let values = bucket.values; 59 | if(values) 60 | length++; 61 | } 62 | return length; 63 | } 64 | } 65 | 66 | class PercentileSnapshot { 67 | private stats: Stats; 68 | private mean; 69 | private p0 :number; 70 | private p5 :number; 71 | private p10:number; 72 | private p25:number; 73 | private p50:number; 74 | private p75:number; 75 | private p90:number; 76 | private p95:number; 77 | private p99:number; 78 | private p995: number; 79 | private p999: number; 80 | private p100: number; 81 | 82 | constructor(allBuckets = []) { 83 | this.stats = new Stats(); 84 | for (let bucket of allBuckets) { 85 | let values = bucket.values; 86 | if (values) 87 | this.stats.push(values); 88 | } 89 | 90 | this.mean = this.stats.amean() || 0; 91 | this.p0 = this.stats.percentile(0) || 0; 92 | this.p5 = this.stats.percentile(5) || 0; 93 | this.p10 = this.stats.percentile(10) || 0; 94 | this.p25 = this.stats.percentile(25) || 0; 95 | this.p50 = this.stats.percentile(50) || 0; 96 | this.p75 = this.stats.percentile(75) || 0; 97 | this.p90 = this.stats.percentile(90) || 0; 98 | this.p95 = this.stats.percentile(95) || 0; 99 | this.p99 = this.stats.percentile(99) || 0; 100 | this.p995 = this.stats.percentile(99.5) || 0; 101 | this.p999 = this.stats.percentile(99.9) || 0; 102 | this.p100 = this.stats.percentile(100) || 0; 103 | 104 | } 105 | 106 | getPercentile(percentile: number | string = "mean") { 107 | if (percentile === "mean") { 108 | return this.mean; 109 | } 110 | 111 | switch (percentile) { 112 | case 0: return this.p0; 113 | case 5: return this.p5; 114 | case 10: return this.p10; 115 | case 25: return this.p25; 116 | case 50: return this.p50; 117 | case 75: return this.p75; 118 | case 90: return this.p90; 119 | case 95: return this.p95; 120 | case 99: return this.p99; 121 | case 99.5: return this.p995; 122 | case 99.9: return this.p999; 123 | case 100: return this.p100; 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /src/commands/semaphore.ts: -------------------------------------------------------------------------------- 1 | import { CommandProperties } from './commandProperties'; 2 | 3 | export class Semaphore { 4 | private currentExecution = 0; 5 | private currentFallback = 0; 6 | 7 | constructor(private maxExecution: number, private maxFallback: number) { } 8 | 9 | canExecuteCommand() { 10 | if( this.maxExecution !== 0 && this.currentExecution > this.maxExecution) { 11 | return false; 12 | } 13 | this.currentExecution++; 14 | return true; 15 | } 16 | 17 | releaseExecutionCommand() { 18 | this.currentExecution--; 19 | } 20 | 21 | canExecuteFallback() { 22 | if( this.maxFallback !== 0 && this.currentFallback > this.maxFallback) { 23 | return false; 24 | } 25 | this.currentFallback++; 26 | return true; 27 | } 28 | 29 | releaseFallback() { 30 | this.currentFallback--; 31 | } 32 | 33 | } 34 | 35 | export class SemaphoreFactory { 36 | private static semaphores = new Map(); 37 | 38 | static getOrCreate(info:CommandProperties) { 39 | 40 | let previouslyCached = SemaphoreFactory.semaphores.get(info.commandName); 41 | if (previouslyCached) { 42 | return previouslyCached; 43 | } 44 | let semaphore = new Semaphore(info.executionIsolationSemaphoreMaxConcurrentRequests.value, info.fallbackIsolationSemaphoreMaxConcurrentRequests.value); 45 | SemaphoreFactory.semaphores.set(info.commandName, semaphore); 46 | return semaphore; 47 | } 48 | 49 | static resetCache() { 50 | SemaphoreFactory.semaphores.clear(); 51 | } 52 | } -------------------------------------------------------------------------------- /src/commands/types.ts: -------------------------------------------------------------------------------- 1 | export interface IHttpCommandResponse { 2 | body: any; 3 | ok: boolean; 4 | code: number; 5 | status: number; 6 | statusType: number; 7 | info: boolean; 8 | clientError: boolean; 9 | serverError: boolean; 10 | accepted: boolean; 11 | noContent: boolean; 12 | badRequest: boolean; 13 | unauthorized: boolean; 14 | notAcceptable: boolean; 15 | notFound: boolean; 16 | forbidden: boolean; 17 | error: any; 18 | cookies: any; 19 | httpVersion: string; 20 | httpVersionMajor: number; 21 | httpVersionMinor: number; 22 | cookie(name: string): string; 23 | headers: any; 24 | raw_body: any; 25 | url: string; 26 | method: string; 27 | socket: any; 28 | client: any; 29 | connection: any; 30 | on(evt: string, callback: (data: any) => void); 31 | } 32 | 33 | export interface IHttpCommandRequest { 34 | url(url: string): IHttpCommandRequest; 35 | method(verb: string): IHttpCommandRequest; 36 | form(options: any): IHttpCommandRequest; 37 | maxRedirects(nb: number): IHttpCommandRequest; 38 | followRedirect(flag: boolean): IHttpCommandRequest; 39 | encoding(encoding: string): IHttpCommandRequest; 40 | strictSSL(flag: boolean): IHttpCommandRequest; 41 | httpSignature(data: any): IHttpCommandRequest; 42 | secureProtocol(protocol:string): IHttpCommandRequest; 43 | proxy(proxy: string): IHttpCommandRequest; 44 | timeout(ms: number): IHttpCommandRequest; 45 | send(data?: any):IHttpCommandRequest; 46 | end(callback: (response: IHttpCommandResponse) => void); 47 | on(evt: string, callback: (data: any) => void); 48 | hasHeader(name: string): boolean; 49 | stream(): IHttpCommandRequest; 50 | field(name: string, value: any, options?):IHttpCommandRequest; 51 | attach(name: string, path: string, options?):IHttpCommandRequest; 52 | rawField(name: string, value: any, options?):IHttpCommandRequest; 53 | auth(user: string, password: string, sendImmediately?: boolean): IHttpCommandRequest; 54 | header(name: string|any, value?: string): IHttpCommandRequest; 55 | query(value: string): IHttpCommandRequest; 56 | type(type: string): IHttpCommandRequest; 57 | part(options: string | any): IHttpCommandRequest; 58 | json(data):any; 59 | } 60 | -------------------------------------------------------------------------------- /src/configurations/abstractions.ts: -------------------------------------------------------------------------------- 1 | import * as rx from 'rxjs'; 2 | 3 | export interface IConfigurationSource { 4 | get(name: string): any; 5 | } 6 | 7 | export interface ILocalConfigurationSource extends IConfigurationSource { 8 | readProperties(timeout?: number): Promise; 9 | } 10 | 11 | export interface IRemoteConfigurationSource extends IConfigurationSource { 12 | pollProperties(timeout?: number): Promise; 13 | } 14 | 15 | 16 | export interface ConfigurationItem { 17 | key: string; 18 | value: any; 19 | lastUpdate?: string; 20 | encrypted?: boolean; 21 | deleted?: boolean; 22 | } 23 | 24 | ///

25 | /// This class represents a result from a poll of configuration source 26 | /// 27 | export class DataSource { 28 | 29 | public constructor(public values?: IterableIterator) { 30 | } 31 | } 32 | 33 | export interface IDynamicProperty { 34 | name: string; 35 | value: T; 36 | propertyChanged: rx.Observable>; 37 | set(val: T): void; 38 | } -------------------------------------------------------------------------------- /src/configurations/configurationSourceBuilder.ts: -------------------------------------------------------------------------------- 1 | import { ConfigurationManager } from './configurationManager'; 2 | import { IConfigurationSource, IRemoteConfigurationSource } from './abstractions'; 3 | import { ConfigurationDataType, FileConfigurationSource } from './sources/fileConfigurationSource'; 4 | import { VulcainConfigurationSource } from './sources/vulcainConfigurationSource'; 5 | import { Service } from '../globals/system'; 6 | 7 | /** 8 | * Helper for adding configuration source providing by DynamicConfiguration.init 9 | */ 10 | export class ConfigurationSourceBuilder { 11 | private _sources: Array; 12 | 13 | constructor(private _configurationManager: ConfigurationManager) { 14 | this._sources = []; 15 | this.addVulcainSource(); 16 | } 17 | 18 | public addSource(source: IRemoteConfigurationSource) { 19 | this._sources.push(source); 20 | return this; 21 | } 22 | 23 | private addVulcainSource() { 24 | if (Service.vulcainServer) { 25 | if (!Service.vulcainToken && !Service.isTestEnvironment) { 26 | Service.log.info(null, () => "No token defined for reading configuration properties. Vulcain configuration source is ignored."); 27 | } 28 | else { 29 | let uri = `http://${Service.vulcainServer}/api/configforservice`; 30 | let options = { 31 | environment: Service.environment, 32 | service: Service.serviceName, 33 | version: Service.serviceVersion, 34 | domain: Service.domainName 35 | }; 36 | this.addSource(new VulcainConfigurationSource(uri, options)); 37 | } 38 | } 39 | 40 | return this; 41 | } 42 | 43 | /*public addRestSource(uri:string) 44 | { 45 | this.addSource(new HttpConfigurationSource(uri)); 46 | return this; 47 | }*/ 48 | 49 | public addFileSource(path: string, mode: ConfigurationDataType = ConfigurationDataType.Json) { 50 | this._sources.push(new FileConfigurationSource(path, mode)); 51 | return this; 52 | } 53 | 54 | public startPolling() { 55 | return this._configurationManager.startPolling(this._sources); 56 | } 57 | } -------------------------------------------------------------------------------- /src/configurations/properties/chainedPropertyValue.ts: -------------------------------------------------------------------------------- 1 | import * as rx from 'rxjs'; 2 | import { IDynamicProperty } from '../abstractions'; 3 | import { ConfigurationManager } from '../configurationManager'; 4 | import { DynamicProperty } from './dynamicProperty'; 5 | import { Observable } from 'rxjs/Observable'; 6 | 7 | export class ChainedDynamicProperty extends DynamicProperty { 8 | private _activeProperty: IDynamicProperty; 9 | 10 | constructor(manager: ConfigurationManager, name: string, private _fallbackProperties: Array, defaultValue?) { 11 | super(manager, name, undefined); 12 | if (this._fallbackProperties.indexOf(name) < 0) 13 | this._fallbackProperties.unshift(name); 14 | if (this._fallbackProperties.length < 1) throw new Error("You must provided at least 1 property."); 15 | 16 | this.defaultValue = defaultValue; 17 | 18 | this.reset(); 19 | 20 | manager.properties.set(name, this); 21 | 22 | // subscribe to changes 23 | manager.propertyChanged.subscribe(this.reset.bind(this)); 24 | } 25 | 26 | reset(dp?: IDynamicProperty) { 27 | if (this.notifying) 28 | return; 29 | 30 | if (dp && this._fallbackProperties.indexOf(dp.name) < 0) 31 | return; 32 | 33 | this.notifying = true; 34 | this._activeProperty = null; 35 | const oldValue = this.value; 36 | 37 | // Find first property value in the chain 38 | for (let propertyName of this._fallbackProperties) { 39 | if (propertyName === this.name) { 40 | if (this.val !== undefined) { 41 | this._activeProperty = this; 42 | break; 43 | } 44 | } 45 | else { 46 | let tmp = this.manager.getProperty(propertyName); 47 | if (tmp && tmp.value !== undefined) { 48 | this._activeProperty = tmp; 49 | break; 50 | } 51 | } 52 | } 53 | 54 | if (oldValue !== this.value) 55 | this.onPropertyChanged(); 56 | 57 | this.notifying = false; 58 | } 59 | 60 | get value() { 61 | let v; 62 | if (this.removed) 63 | return undefined; 64 | if (this.val !== undefined) 65 | v = this.val; 66 | else if (this._activeProperty) 67 | v = this._activeProperty.value; 68 | return v || this.defaultValue; 69 | } 70 | 71 | public toJSON(key: string) { 72 | return key ? String(this.value) : this.name + "=" + String(this.value); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/configurations/properties/dynamicProperty.ts: -------------------------------------------------------------------------------- 1 | import { ConfigurationItem, IDynamicProperty } from "../abstractions"; 2 | import { ConfigurationManager } from "../configurationManager"; 3 | import * as rx from 'rxjs'; 4 | import { Service } from "../../globals/system"; 5 | 6 | export interface IUpdatableProperty { // Internal interface 7 | updateValue(val: ConfigurationItem); 8 | } 9 | 10 | export class DynamicProperty implements IDynamicProperty, IUpdatableProperty { 11 | protected val: T; 12 | protected removed: boolean; 13 | protected notifying: boolean; 14 | private _propertyChanged: rx.ReplaySubject>; 15 | 16 | constructor(protected manager: ConfigurationManager, public name: string, protected defaultValue: T) { 17 | manager.properties.set(name, this); 18 | if(this.defaultValue !== undefined) 19 | this.onPropertyChanged(); 20 | } 21 | 22 | get propertyChanged(): rx.Observable> { 23 | if (!this._propertyChanged) { 24 | this._propertyChanged = new rx.ReplaySubject>(1); 25 | } 26 | return >>this._propertyChanged; 27 | } 28 | 29 | get value() { 30 | return !this.removed ? (this.val||this.defaultValue) : undefined; 31 | } 32 | 33 | set(val: T) { 34 | if (this.val !== val) { 35 | this.val = val; 36 | this.onPropertyChanged(); 37 | } 38 | } 39 | 40 | updateValue(item: ConfigurationItem) { 41 | if (item.deleted) { 42 | this.removed = true; 43 | Service.log.info(null, () => `CONFIG: Removing property value for key ${this.name}`); 44 | this.onPropertyChanged(); 45 | return; 46 | } 47 | 48 | if (this.val !== item.value) { 49 | this.val = item.encrypted ? Service.decrypt(item.value) : item.value; 50 | let v = item.encrypted ? "********" : item.value; 51 | Service.log.info(null, () => `CONFIG: Setting property value '${v}' for key ${this.name}`); 52 | this.onPropertyChanged(); 53 | return; 54 | } 55 | } 56 | 57 | protected onPropertyChanged() { 58 | if (!this.name || this.notifying) 59 | return; 60 | 61 | this.notifying = true; 62 | try { 63 | this._propertyChanged && this._propertyChanged.next(this); 64 | this.manager.onPropertyChanged(this); 65 | } 66 | finally { 67 | this.notifying = false; 68 | } 69 | } 70 | 71 | public dispose() { 72 | this.onPropertyChanged(); 73 | //this._propertyChanged.dispose(); 74 | this._propertyChanged = null; 75 | this.removed = true; 76 | } 77 | 78 | public toJSON(key: string) { 79 | return key ? String(this.value) : this.name + "=" + String(this.value); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/configurations/sources/PrioritizedSourceValue.ts: -------------------------------------------------------------------------------- 1 | import { IConfigurationSource, IRemoteConfigurationSource } from "../abstractions"; 2 | 3 | export class PrioritizedSourceValue implements IConfigurationSource { 4 | private chain: IConfigurationSource[]; 5 | 6 | get remoteSources() { 7 | return this._remoteSources; 8 | } 9 | 10 | constructor( localSources?: IConfigurationSource[], private _remoteSources?: IRemoteConfigurationSource[]) { 11 | this.chain = []; 12 | if (_remoteSources) 13 | this.chain = this.chain.concat(_remoteSources); 14 | else { 15 | this._remoteSources = []; 16 | } 17 | if (localSources) 18 | this.chain = this.chain.concat(localSources); 19 | } 20 | 21 | get(name: string) { 22 | for (let pv of this.chain) { 23 | let val = pv.get(name); 24 | if (val !== undefined) return val; 25 | } 26 | return undefined; 27 | } 28 | } -------------------------------------------------------------------------------- /src/configurations/sources/abstractRemoteSource.ts: -------------------------------------------------------------------------------- 1 | import { IRemoteConfigurationSource, ConfigurationItem, DataSource } from "../abstractions"; 2 | 3 | export abstract class AbstractRemoteSource implements IRemoteConfigurationSource { 4 | private _values = new Map(); 5 | 6 | abstract pollProperties(timeout?: number): Promise; 7 | 8 | get(name: string) { 9 | let v = this._values.get(name); 10 | return (v && v.value) || undefined; 11 | } 12 | 13 | protected mergeChanges(changes: Map) { 14 | changes && changes.forEach(item => { 15 | if (!item.deleted) 16 | this._values.set(item.key, item); 17 | else 18 | this._values.delete(item.key); 19 | }); 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/configurations/sources/environmentVariableSource.ts: -------------------------------------------------------------------------------- 1 | import { IConfigurationSource } from "../abstractions"; 2 | import * as fs from 'fs'; 3 | const NONE = "$$__none__$$"; 4 | 5 | export class EnvironmentVariableSource implements IConfigurationSource { 6 | 7 | get(name: string):any { 8 | // As is 9 | let env = process.env[name]; 10 | if (env) { 11 | if (env === NONE) 12 | return undefined; 13 | return env; 14 | } 15 | 16 | // Replace dot 17 | env = process.env[name.replace(/\./g, '_')]; 18 | if (env) 19 | return env; 20 | 21 | // Replace dot with uppercases 22 | env = process.env[name.toUpperCase().replace(/\./g, '_')]; 23 | if (env) 24 | return env; 25 | 26 | // Transform camel case to upper case 27 | // ex: myProperty --> MY_PROPERTY 28 | const regex = /([A-Z])|(\.)/g; 29 | const subst = `_\$1`; 30 | let res = name.replace(regex, subst); 31 | env = process.env[res.toUpperCase()]; 32 | 33 | // Otherwise as a docker secret 34 | if (env === undefined) { 35 | try { 36 | // Using sync method here is assumed 37 | env = fs.readFileSync('/run/secrets/' + name, { encoding: 'utf8', flag: 'r' }); 38 | } 39 | catch (e) { 40 | // ignore error 41 | } 42 | } 43 | if (env === undefined) { 44 | // Set cache to avoid many file reads 45 | process.env[name] = NONE; 46 | } 47 | return env; 48 | } 49 | } -------------------------------------------------------------------------------- /src/configurations/sources/httpConfigurationSource.ts: -------------------------------------------------------------------------------- 1 | import { IRemoteConfigurationSource, DataSource, ConfigurationItem } from "../abstractions"; 2 | import { AbstractRemoteSource } from "./abstractRemoteSource"; 3 | import { Service } from "../../globals/system"; 4 | 5 | const rest = require('unirest'); 6 | 7 | export class HttpConfigurationSource extends AbstractRemoteSource { 8 | protected lastUpdate: string; 9 | 10 | constructor(protected uri: string) { 11 | super(); 12 | } 13 | 14 | protected prepareRequest(request) { 15 | return request; 16 | } 17 | 18 | protected createRequestUrl() { 19 | let uri = this.uri; 20 | if (this.lastUpdate) { 21 | uri = uri + "?lastUpdate=" + this.lastUpdate; 22 | } 23 | return uri; 24 | } 25 | 26 | async pollProperties(timeoutInMs: number) { 27 | let self = this; 28 | return new Promise((resolve) => { 29 | let uri = this.createRequestUrl(); 30 | 31 | try { 32 | let values: Map; 33 | 34 | let request = rest.get(uri) 35 | .headers({ 'Accept': 'application/json' }) 36 | .timeout(timeoutInMs); 37 | 38 | request = this.prepareRequest(request); 39 | 40 | request.end(function (response) { 41 | if (response.status === 200 && response.body) { 42 | if (response.body.error) { 43 | if (!Service.isDevelopment) { 44 | Service.log.info(null, () => `HTTP CONFIG : error when polling properties on ${uri} - ${response.body.error.message}`); 45 | } 46 | } 47 | else { 48 | values = new Map(); 49 | let data = response.body; 50 | data.value && data.value.forEach(cfg => values.set(cfg.key, cfg)); 51 | self.lastUpdate = Service.nowAsString(); 52 | self.mergeChanges(values); 53 | } 54 | } 55 | else { 56 | Service.log.info(null, () => `HTTP CONFIG : error when polling properties on ${uri} - ${(response.error && response.error.message) || response.status}`); 57 | } 58 | resolve((values && new DataSource(values.values())) || null); 59 | }); 60 | } 61 | catch (e) { 62 | Service.log.info(null, () => `HTTP CONFIG : error when polling properties on ${uri} - ${e.message}`); 63 | resolve(null); 64 | } 65 | }); 66 | } 67 | } -------------------------------------------------------------------------------- /src/configurations/sources/memoryConfigurationSource.ts: -------------------------------------------------------------------------------- 1 | import { ILocalConfigurationSource, ConfigurationItem, IRemoteConfigurationSource, DataSource } from "../abstractions"; 2 | 3 | 4 | export class MemoryConfigurationSource implements ILocalConfigurationSource { 5 | protected _values = new Map(); 6 | readProperties(timeout?: number): Promise { 7 | return Promise.resolve( new DataSource(this._values.values())); 8 | } 9 | 10 | /// 11 | /// Set a update a new property 12 | /// 13 | /// Property name 14 | /// Property value 15 | set(name: string, value: any) { 16 | this._values.set(name, { value, key: name }); 17 | } 18 | 19 | get(name: string) { 20 | let v = this._values.get(name); 21 | return (v && v.value) || undefined; 22 | } 23 | } 24 | 25 | export class MockConfigurationSource extends MemoryConfigurationSource implements IRemoteConfigurationSource { 26 | pollProperties(timeout?: number): Promise { 27 | return this.readProperties(); 28 | } 29 | } -------------------------------------------------------------------------------- /src/configurations/sources/vulcainConfigurationSource.ts: -------------------------------------------------------------------------------- 1 | import { HttpConfigurationSource } from './httpConfigurationSource'; 2 | import { DataSource } from '../abstractions'; 3 | import { Service } from '../../globals/system'; 4 | const rest = require('unirest'); 5 | 6 | export class VulcainConfigurationSource extends HttpConfigurationSource { 7 | 8 | constructor(uri: string, private options) { 9 | super(uri); 10 | } 11 | 12 | protected prepareRequest(request) { 13 | if(Service.vulcainToken) 14 | request = request.headers({ Authorization: 'ApiKey ' + Service.vulcainToken }); 15 | return request; 16 | } 17 | 18 | protected createRequestUrl() { 19 | this.options.lastUpdate = this.lastUpdate; 20 | return this.uri + "?$query=" + JSON.stringify(this.options); 21 | } 22 | 23 | pollProperties(timeoutInMs: number) { 24 | if (!Service.vulcainToken && !Service.isTestEnvironment) { 25 | return Promise.resolve(null); 26 | } 27 | 28 | return super.pollProperties(timeoutInMs); 29 | } 30 | } -------------------------------------------------------------------------------- /src/defaults/dependencyExplorer.ts: -------------------------------------------------------------------------------- 1 | import { LifeTime } from '../di/annotations'; 2 | import { Service } from './../globals/system'; 3 | import { VulcainManifest } from '../globals/manifest'; 4 | import { RequestContext } from "../pipeline/requestContext"; 5 | import { ForbiddenRequestError } from "../pipeline/errors/applicationRequestError"; 6 | import { Query } from '../pipeline/handlers/query/annotations.query'; 7 | import { QueryHandler } from '../pipeline/handlers/query/annotations.queryHandler'; 8 | 9 | @QueryHandler({ scope: "?", serviceLifeTime: LifeTime.Singleton }, { system: true }) 10 | export class DependencyExplorer { 11 | 12 | constructor() { 13 | } 14 | 15 | @Query({ outputSchema: "VulcainManifest", description: "Get service dependencies", name: "_serviceDependencies" }) 16 | getDependencies() { 17 | let ctx: RequestContext = (this).context; 18 | if (ctx.publicPath) 19 | throw new ForbiddenRequestError(); 20 | 21 | return Service.manifest; 22 | } 23 | } -------------------------------------------------------------------------------- /src/defaults/scopeDescriptors.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Injectable, DefaultServiceNames, LifeTime } from "../di/annotations"; 3 | import { Service } from "../globals/system"; 4 | 5 | export class ScopeDescription { 6 | name: string; 7 | description: string; 8 | domain: string; 9 | } 10 | 11 | @Injectable(LifeTime.Singleton, DefaultServiceNames.ScopesDescriptor) 12 | export class ScopesDescriptor { 13 | private scopes = new Array(); 14 | 15 | getScopes() { 16 | return this.scopes; 17 | } 18 | 19 | defineScope(name: string, description: string) { 20 | this.scopes.push({ name: Service.domainName + ":" + name, description, domain: Service.domainName }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/defaults/serviceExplorer.ts: -------------------------------------------------------------------------------- 1 | import { DefaultServiceNames, Inject, LifeTime } from '../di/annotations'; 2 | import { IContainer } from "../di/resolvers"; 3 | import { Domain } from '../schemas/domain'; 4 | import { RequestContext } from "../pipeline/requestContext"; 5 | import { ForbiddenRequestError } from "../pipeline/errors/applicationRequestError"; 6 | import { ServiceDescriptors } from "../pipeline/handlers/descriptions/serviceDescriptions"; 7 | import { ServiceDescription } from "../pipeline/handlers/descriptions/serviceDescription"; 8 | import { SwaggerServiceDescriptor } from './swagger/swaggerServiceDescriptions'; 9 | import { SwaggerApiDefinition } from './swagger/swaggerApiDefinition'; 10 | import { HttpResponse, Metadata } from '../index'; 11 | import { Model, InputModel } from '../schemas/builder/annotations.model'; 12 | import { Property } from '../schemas/builder/annotations.property'; 13 | import { Query } from '../pipeline/handlers/query/annotations.query'; 14 | import { QueryHandler } from '../pipeline/handlers/query/annotations.queryHandler'; 15 | 16 | @InputModel() 17 | @Metadata("system", true) 18 | export class ServiceExplorerParameter { 19 | @Property({ description: "Format the description service. Only 'swagger' are available", type: "string" }) 20 | format: string; 21 | } 22 | 23 | @QueryHandler({ scope: "?", serviceLifeTime: LifeTime.Singleton }, {system:true}) 24 | export class ServiceExplorer { 25 | 26 | constructor( @Inject(DefaultServiceNames.Domain) private domain: Domain, 27 | @Inject(DefaultServiceNames.Container) private container: IContainer) { 28 | } 29 | 30 | @Query({ outputSchema: "ServiceDescription", description: "Get all service handler description. You can get the response in swagger format with format=swagger", name: "_serviceDescription" }) 31 | async getServiceDescriptions(model: ServiceExplorerParameter) { 32 | let ctx: RequestContext = (this).context; 33 | if (ctx.publicPath) 34 | throw new ForbiddenRequestError(); 35 | 36 | let descriptors = this.container.get(DefaultServiceNames.ServiceDescriptors); 37 | let result: ServiceDescription = await descriptors.getDescriptions(false); 38 | result.alternateAddress = (this).context.hostName; 39 | 40 | if (model.format === 'swagger') { 41 | let descriptors = this.container.get(DefaultServiceNames.SwaggerServiceDescriptor); 42 | let swaggerResult: SwaggerApiDefinition = await descriptors.getDescriptions(result); 43 | let response = new HttpResponse(); 44 | response.content = swaggerResult; 45 | return response; 46 | } else { 47 | return result; 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/defaults/swagger/_swaggerTemplate.ts: -------------------------------------------------------------------------------- 1 | import * as swaggerUI from 'swagger-ui-dist'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | 5 | const swaggerUiAbsolutePath = swaggerUI.getAbsoluteFSPath(); 6 | 7 | // tslint:disable-next-line:class-name 8 | export class SwaggerTemplate { 9 | 10 | static getHtmlRendered(title: string, url: string) { 11 | return ` 12 | 13 | 14 | 15 | 16 | 17 | ${title} 18 | 19 | 20 | 21 | 24 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 |
81 | 82 | 83 | 84 | 103 | 104 | 105 | 106 | `; 107 | } 108 | } -------------------------------------------------------------------------------- /src/defaults/swagger/swaggerUIHandler.ts: -------------------------------------------------------------------------------- 1 | import { LifeTime } from '../../di/annotations'; 2 | import { Service } from '../../globals/system'; 3 | import { VulcainManifest } from '../../globals/manifest'; 4 | import { RequestContext } from "../../pipeline/requestContext"; 5 | import { ForbiddenRequestError } from "../../pipeline/errors/applicationRequestError"; 6 | import { HttpResponse } from "../../pipeline/response"; 7 | import { Query } from '../../pipeline/handlers/query/annotations.query'; 8 | import { QueryHandler } from '../../pipeline/handlers/query/annotations.queryHandler'; 9 | 10 | @QueryHandler({ scope: "?", serviceLifeTime: LifeTime.Singleton }) 11 | export class SwaggerUIHandler { 12 | 13 | @Query({ outputSchema: "string", description: "Display Swagger UI", name: "_swagger" }, {system:true}) 14 | displaySwaggerUI() { 15 | let ctx: RequestContext = (this).context; 16 | if (ctx.publicPath) 17 | throw new ForbiddenRequestError(); 18 | 19 | let url = '/api/_servicedescription?format=swagger'; 20 | 21 | let template = require('./_swaggerTemplate').SwaggerTemplate; 22 | 23 | let response = new HttpResponse(template.getHtmlRendered('Vulcainjs - Swagger UI', url)); 24 | response.contentType = "text/html"; 25 | return response; 26 | } 27 | } -------------------------------------------------------------------------------- /src/di/annotations.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Preloader } from '../preloader'; 3 | import { Service } from '../globals/system'; 4 | import { RequestContext } from "../pipeline/requestContext"; 5 | import { IRequestContext } from "../pipeline/common"; 6 | 7 | /** 8 | * List of default service names 9 | * 10 | * @export 11 | * @class DefaultServiceNames 12 | */ 13 | export class DefaultServiceNames { 14 | static Serializer = "Serializer"; 15 | static AuthenticationStrategy = "AuthenticationStrategy"; 16 | static AuthorizationPolicy = "AuthorizationPolicy"; 17 | static TenantPolicy = "TenantPolicy"; 18 | static TaskManager = "TaskManager"; 19 | static ScopesDescriptor = "ScopesDescriptor"; 20 | static ServiceDescriptors = "ServiceDescriptors"; 21 | static SwaggerServiceDescriptor = "SwaggerServiceDescriptor"; 22 | static SecurityManager = "SecurityManager"; 23 | static Logger = "Logger"; 24 | static EventBusAdapter = "EventBusAdapter"; 25 | static ActionBusAdapter = "ActionBusAdapter"; 26 | static Domain = "Domain"; 27 | static Application = "ApplicationFactory"; 28 | static ServerAdapter = "ServerAdapter"; 29 | static Container = "Container"; 30 | static ProviderFactory = "ProviderFactory"; 31 | static Metrics = "Metrics"; 32 | static StubManager = "StubManager"; 33 | static RequestTracker = "RequestTracker"; 34 | static ServiceResolver = "ServiceResolver"; 35 | static BearerTokenService = "BearerTokenService"; 36 | static HandlerProcessor = "HandlerProcessor"; 37 | static GraphQLAdapter = "GraphQLAdapter"; 38 | } 39 | 40 | /** 41 | * Component life time 42 | * 43 | * @export 44 | * @enum {number} 45 | */ 46 | export enum LifeTime { 47 | /** 48 | * Only one instance 49 | */ 50 | Singleton = 1, 51 | /** 52 | * Create a new instance every time 53 | */ 54 | Transient = 2, 55 | /** 56 | * Create one instance per request 57 | */ 58 | Scoped = 4 59 | } 60 | 61 | /** 62 | * Interface implemented by every scoped component 63 | * 64 | * @export 65 | * @interface IScopedComponent 66 | */ 67 | export interface IScopedComponent { 68 | /** 69 | * Current request context (scope) 70 | * 71 | * @type {IRequestContext} 72 | */ 73 | context: IRequestContext; 74 | } 75 | 76 | /** 77 | * Used to initialize a constructor parameter with a component 78 | * 79 | * @export 80 | * @param {string} component name 81 | * @param {boolean} [optional] True to not raise an exception if component doesn't exist 82 | */ 83 | export function Inject(optional?: boolean); 84 | export function Inject(name: string, optional?: boolean); 85 | export function Inject(nameOrBool?: string | boolean, optional?: boolean) { 86 | let name: string; 87 | if (typeof nameOrBool === "string") { 88 | name = nameOrBool; 89 | } 90 | else { 91 | optional = nameOrBool; 92 | } 93 | return function (target, key, i?) { 94 | if (i !== undefined) { 95 | // Constructor injection 96 | let injects = Reflect.getOwnMetadata(Symbol.for("di:ctor_injects"), target) || []; 97 | injects[i] = { name: name, optional: !!optional }; 98 | Reflect.defineMetadata(Symbol.for("di:ctor_injects"), injects, target); 99 | } 100 | else { 101 | // Property constructor 102 | let injects = Reflect.getOwnMetadata(Symbol.for("di:props_injects"), target) || []; 103 | injects.push({ name: name || Reflect.getOwnMetadata("design:type", target, key).name, optional: !!optional, property: key }); 104 | Reflect.defineMetadata(Symbol.for("di:props_injects"), injects, target); 105 | } 106 | }; 107 | } 108 | 109 | /** 110 | * Used to declare a component. 111 | * 112 | * @export 113 | * @param {LifeTime} lifeTime of the component 114 | * @param {string} [name] - By default this is the class name 115 | * @param {enableOnTestOnly} Active this component only in an test environment 116 | */ 117 | export function Injectable(lifeTime: LifeTime, name?: string, enableOnTestOnly?: boolean) { 118 | return function (target) { 119 | if (enableOnTestOnly && !Service.isTestEnvironment) 120 | return; 121 | name = name || target.name; 122 | Preloader.instance.registerService((container, domain) => { 123 | container.inject(name, target, lifeTime); 124 | } 125 | ); 126 | }; 127 | } 128 | -------------------------------------------------------------------------------- /src/di/scope.ts: -------------------------------------------------------------------------------- 1 | import { RequestContext } from "../pipeline/requestContext"; 2 | 3 | export class Scope { 4 | private cache = new Map(); 5 | 6 | constructor(private parent: Scope, public context: RequestContext) { } 7 | 8 | getInstance(name: string) { 9 | if (!name) 10 | throw new Error("name argument must not be null"); 11 | let component = this.cache.get(name); 12 | return component || this.parent && this.parent.getInstance(name); 13 | } 14 | 15 | set(name: string, component) { 16 | if (!name) 17 | throw new Error("name argument must not be null"); 18 | this.cache.set(name, component); 19 | } 20 | 21 | dispose() { 22 | this.cache.forEach(v => v.dispose && v.dispose()); 23 | this.cache.clear(); 24 | this.parent = null; 25 | this.context = null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/di/serviceResolver.ts: -------------------------------------------------------------------------------- 1 | import { Service } from '../globals/system'; 2 | 3 | export interface IServiceResolver { 4 | resolve(serviceName: string, version: string): Promise; 5 | } 6 | 7 | export class ServiceResolver implements IServiceResolver { 8 | /** 9 | * 10 | * 11 | * @private 12 | * @param {string} serviceName 13 | * @param {number} version 14 | * @returns 15 | */ 16 | resolve(serviceName: string, version: string) { 17 | if (!serviceName) 18 | throw new Error("You must provide a service name"); 19 | if (!version || !version.match(/^[0-9]+\.[0-9]+$/)) 20 | throw new Error("Invalid version number. Must be on the form major.minor"); 21 | 22 | return Promise.resolve(Service.createContainerEndpoint(serviceName, version)); 23 | } 24 | } -------------------------------------------------------------------------------- /src/graphql/graphQLHandler.ts: -------------------------------------------------------------------------------- 1 | import { AbstractHandler } from "../pipeline/handlers/abstractHandlers"; 2 | import { DefaultServiceNames } from "../di/annotations"; 3 | import { HttpResponse } from "../pipeline/response"; 4 | import { ActionHandler, Action, ExposeEvent } from "../pipeline/handlers/action/annotations"; 5 | import { ApplicationError } from "../pipeline/errors/applicationRequestError"; 6 | import { ISpanRequestTracker } from "../instrumentations/common"; 7 | import { IContainer } from "../di/resolvers"; 8 | import { Inject } from "../di/annotations"; 9 | import { EventNotificationMode } from "../bus/messageBus"; 10 | import { GraphQLAdapter } from "./graphQLAdapter"; 11 | 12 | export class GraphQLActionHandler extends AbstractHandler { 13 | private static _schema; 14 | 15 | constructor( 16 | @Inject(DefaultServiceNames.Container) container: IContainer, 17 | @Inject(DefaultServiceNames.GraphQLAdapter) private _adapter: GraphQLAdapter) { 18 | super(container); 19 | } 20 | 21 | @Action({ description: "Custom action", name: "_graphql" }, {system: true}) 22 | @ExposeEvent({mode: EventNotificationMode.never}) 23 | async graphql(g: any) { 24 | 25 | (this.context.requestTracker).trackAction("graphql"); 26 | 27 | let response = await this._adapter.processGraphQLQuery(this.context, g); 28 | 29 | if (this.metadata.metadata.responseType === "graphql") 30 | return new HttpResponse(response); 31 | 32 | if (response.errors && response.errors.length > 0) { 33 | throw new ApplicationError(response.errors[0].message); 34 | } 35 | return response.data; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/instrumentations/common.ts: -------------------------------------------------------------------------------- 1 | import { IRequestContext } from "../pipeline/common"; 2 | import { ITrackerAdapter } from "../instrumentations/trackers/index"; 3 | import { Service } from "../globals/system"; 4 | 5 | export enum SpanKind { 6 | Request, 7 | Command, 8 | Task, 9 | Event, 10 | Custom 11 | } 12 | 13 | export interface TrackerId { 14 | correlationId?: string; 15 | parentId?: string; 16 | spanId?: string; 17 | } 18 | 19 | export interface ITracker { 20 | /** 21 | * Log an error 22 | * 23 | * @param {Error} error Error instance 24 | * @param {string} [msg] Additional message 25 | * 26 | */ 27 | logError(error: Error, msg?: () => string); 28 | 29 | /** 30 | * Log a message info 31 | * 32 | * @param {string} msg Message format (can include %s, %j ...) 33 | * @param {...Array} params Message parameters 34 | * 35 | */ 36 | logInfo(msg: () => string); 37 | 38 | /** 39 | * Log a verbose message. Verbose message are enable by service configuration property : enableVerboseLog 40 | * 41 | * @param {any} context Current context 42 | * @param {string} msg Message format (can include %s, %j ...) 43 | * @param {...Array} params Message parameters 44 | * 45 | */ 46 | logVerbose(msg: () => string); 47 | 48 | dispose(); 49 | id: TrackerId; 50 | } 51 | 52 | export interface ISpanTracker extends ITracker { 53 | context: IRequestContext; 54 | durationInMs: number; 55 | now: number; 56 | tracker: ITrackerAdapter; 57 | kind: SpanKind; 58 | trackAction(name: string, tags?: {[index:string]:string}); 59 | addTag(name: string, value: string); 60 | 61 | addHttpRequestTags(uri:string, verb:string); 62 | addProviderCommandTags(address:string, schema: string, tenant: string ); 63 | addServiceCommandTags(serviceName: string, serviceVersion: string); 64 | addCustomCommandTags(commandType: string, tags: { [key: string]: string }); 65 | injectHeaders(headers: (name: string | any, value?: string) => any); 66 | } 67 | 68 | export interface ISpanRequestTracker extends ISpanTracker { 69 | createCommandTracker(context: IRequestContext, commandName: string): ISpanRequestTracker; 70 | createCustomTracker(context: IRequestContext, name: string, tags?: { [index: string]: string }): ITracker; 71 | } 72 | 73 | export class DummySpanTracker implements ISpanRequestTracker { 74 | durationInMs: number = 0; 75 | now: number; 76 | tracker: ITrackerAdapter; 77 | kind: SpanKind; 78 | 79 | get id(): TrackerId { 80 | return { spanId: "0", parentId: "0" }; 81 | } 82 | 83 | constructor(public context: IRequestContext) { } 84 | 85 | createCustomTracker(context: IRequestContext, name: string, tags?: { [index: string]: string }): ITracker { 86 | return null; 87 | } 88 | 89 | createCommandTracker(context: IRequestContext, commandName: string): ISpanRequestTracker { 90 | return this; 91 | } 92 | trackAction(name: string, tags?: { [index: string]: string }) { 93 | } 94 | addHttpRequestTags(uri: string, verb: string) { } 95 | addProviderCommandTags(address: string, schema: string, tenant: string) { } 96 | addServiceCommandTags(serviceName: string, serviceVersion: string) { } 97 | addCustomCommandTags(commandType: string, tags: { [key: string]: string }) { } 98 | addTag(key: string, value: string) { 99 | } 100 | injectHeaders(headers: (name: any, value?: string) => any) { 101 | } 102 | logError(error: Error, msg?: () => string) { 103 | Service.log.error(this.context, error, msg); 104 | } 105 | logInfo(msg: () => string) { 106 | Service.log.info(this.context, msg); 107 | } 108 | logVerbose(msg: () => string) { 109 | Service.log.verbose(this.context, msg); 110 | } 111 | dispose() { 112 | } 113 | } -------------------------------------------------------------------------------- /src/instrumentations/metrics/consoleMetrics.ts: -------------------------------------------------------------------------------- 1 | import { Service } from '../../globals/system'; 2 | import { IMetrics } from '../metrics'; 3 | 4 | /** 5 | * Metrics adapter for testing 6 | * Emit metrics on console 7 | * 8 | * @export 9 | * @class ConsoleMetrics 10 | */ 11 | export class ConsoleMetrics implements IMetrics { 12 | private tags: string; 13 | 14 | constructor(address?: string) { 15 | this.tags = ",env=" + Service.environment + ",service=" + Service.serviceName + ',version=' + Service.serviceVersion; 16 | } 17 | 18 | private log(msg: string) { 19 | } 20 | 21 | gauge(metric: string, customTags?: any, delta?: number) { 22 | this.log(`METRICS: gauge ${metric + this.tags + customTags} : ${delta || 1}`); 23 | } 24 | 25 | count(metric: string, customTags?: any, delta?: number) { 26 | this.log(`METRICS: counter ${metric + this.tags + customTags} : ${delta||1}`); 27 | } 28 | 29 | timing(metric:string, duration:number, customTags?: string) { 30 | this.log(`METRICS: timing ${metric + this.tags + customTags} : ${duration}ms`); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/instrumentations/metrics/index.ts: -------------------------------------------------------------------------------- 1 | //import { ApplicationInsightsMetrics } from './applicationInsightsMetrics'; 2 | //import { StatsdMetrics } from './statsdMetrics'; 3 | import { PrometheusMetrics } from './prometheusMetrics'; 4 | import { IContainer } from '../../di/resolvers'; 5 | import { DefaultServiceNames } from "../../di/annotations"; 6 | 7 | /** 8 | * Metrics adapter interface 9 | * 10 | * @export 11 | * @interface IMetrics 12 | */ 13 | export interface IMetrics { 14 | gauge(metric: string, value: number, customTags?: any); 15 | /** 16 | * Increment a gauge 17 | * 18 | * @param {string} metric metric name 19 | * @param {number} [delta] default 1 20 | * 21 | * @memberOf IMetrics 22 | */ 23 | count(metric: string, customTags?: any, delta?: number): void; 24 | 25 | /** 26 | * Set a duration 27 | * 28 | * @param {string} metric metric name 29 | * @param {number} [delta] duration in ms 30 | * 31 | * @memberOf IMetrics 32 | */ 33 | timing(metric: string, duration: number, customTags?: any): void; 34 | } 35 | 36 | export class MetricsFactory { 37 | static create(container: IContainer) { 38 | return container.get(DefaultServiceNames.Metrics, true) || 39 | // ApplicationInsightsMetrics.create() || 40 | // StatsdMetrics.create() || 41 | new PrometheusMetrics(container); 42 | } 43 | } -------------------------------------------------------------------------------- /src/instrumentations/metrics/prometheusMetrics.ts: -------------------------------------------------------------------------------- 1 | import { Service } from '../../globals/system'; 2 | import { IMetrics } from '../metrics'; 3 | import * as Prometheus from 'prom-client'; 4 | import { IContainer } from "../../di/resolvers"; 5 | import { HttpRequest } from "../../pipeline/vulcainPipeline"; 6 | import { HttpResponse } from "../../pipeline/response"; 7 | import { DefaultServiceNames } from '../../di/annotations'; 8 | import { VulcainLogger } from '../../log/vulcainLogger'; 9 | import http = require('http'); 10 | 11 | // Avg per seconds 12 | // rate(vulcain_service_duration_seconds_sum[5m]) / rate(vulcain_service_duration_seconds_count[5m]) 13 | // Failed service 14 | // rate(vulcain_service_duration_seconds_count{hasError="true"}[5m]) 15 | export class PrometheusMetrics implements IMetrics { 16 | 17 | private ignoredProperties = ["hystrixProperties", "params"]; 18 | 19 | constructor(private container: IContainer) { 20 | Service.log.info(null, () => `Providing prometheus metrics from '/metrics'`); 21 | 22 | container.registerHTTPEndpoint("GET", '/metrics', (req: http.IncomingMessage, resp: http.ServerResponse) => { 23 | const chunk = new Buffer(Prometheus.register.metrics(), 'utf8'); 24 | resp.setHeader('Content-Type', (Prometheus).contentType || "text/plain; version=0.0.4"); 25 | resp.setHeader('Content-Length', String(chunk.length)); 26 | resp.end(chunk); 27 | }); 28 | } 29 | 30 | private encodeTags(tags: { [key: string] : string }): any { 31 | let result = { service: Service.serviceName, version: Service.serviceVersion, serviceFullName: Service.fullServiceName }; 32 | Object 33 | .keys(tags) 34 | .forEach(key => { 35 | if (this.ignoredProperties.indexOf(key) >= 0) 36 | return; 37 | result[key.replace(/[^a-zA-Z_]/g, '_')] = tags[key]; 38 | }); 39 | return result; 40 | } 41 | 42 | gauge(metric: string, value: number, customTags?: any) { 43 | let labels = this.encodeTags(customTags); 44 | 45 | let gauge:Prometheus.Gauge = (Prometheus.register).getSingleMetric(metric); 46 | if (!gauge) { 47 | gauge = new Prometheus.Gauge({ name: metric, help: metric, labelNames: Object.keys(labels) }); 48 | } 49 | try { 50 | gauge.inc(labels, value); 51 | } 52 | catch (e) { 53 | let logger = this.container.get(DefaultServiceNames.Logger); 54 | logger.error(null, e, () => "Prometheus metrics"); 55 | } 56 | } 57 | 58 | count(metric: string, customTags?: any, delta = 1) { 59 | let labels = this.encodeTags(customTags); 60 | 61 | let counter:Prometheus.Counter = (Prometheus.register).getSingleMetric(metric); 62 | if (!counter) { 63 | counter = new Prometheus.Counter({ name: metric, help: metric, labelNames: Object.keys(labels) }); 64 | } 65 | try { 66 | counter.inc(labels, delta); 67 | } 68 | catch (e) { 69 | let logger = this.container.get(DefaultServiceNames.Logger); 70 | logger.error(null, e, () => "Prometheus metrics"); 71 | } 72 | } 73 | 74 | timing(metric: string, duration: number, customTags?: any) { 75 | let labels = this.encodeTags(customTags); 76 | let counter:Prometheus.Histogram = (Prometheus.register).getSingleMetric(metric); 77 | if (!counter) { 78 | counter = new Prometheus.Histogram({ name: metric, help: metric, labelNames: Object.keys(labels), buckets: [50,100,250,500,1000,1500,2000,5000], }); 79 | } 80 | try { 81 | counter.observe(labels, duration); 82 | } 83 | catch (e) { 84 | let logger = this.container.get(DefaultServiceNames.Logger); 85 | logger.error(null, e, () => "Prometheus metrics"); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/instrumentations/metrics/statsdMetrics.ts: -------------------------------------------------------------------------------- 1 | import { Conventions } from '../../utils/conventions'; 2 | const Statsd = require("statsd-client"); 3 | import { Service } from '../../globals/system'; 4 | import { IMetrics } from '../metrics'; 5 | import { DynamicConfiguration } from '../../configurations/dynamicConfiguration'; 6 | 7 | /** 8 | * Default metrics adapter 9 | * Emit metrics on statsd 10 | * 11 | * @export 12 | * @class StatsdMetrics 13 | */ 14 | /* 15 | export class StatsdMetrics implements IMetrics { 16 | private static EmptyString = ""; 17 | private tags: any; 18 | 19 | constructor(private statsd) { 20 | this.tags = this.encodeTags({ service: System.serviceName, version: System.serviceVersion }); 21 | } 22 | 23 | static create() { 24 | if (!System.isDevelopment) { 25 | let host = DynamicConfiguration.getPropertyValue("statsd"); 26 | if (host) { 27 | let instance = new StatsdMetrics( 28 | new Statsd({ host: host, socketTimeout: Conventions.instance.defaultStatsdDelayInMs })); 29 | System.log.info(null, ()=>"Initialize statsd metrics adapter on '" + host + "' with initial tags : " + instance.tags); 30 | return this; 31 | } 32 | } 33 | return null; 34 | } 35 | 36 | private encodeTags(tags: { [key: string]: string }): any { 37 | if (!tags) 38 | return StatsdMetrics.EmptyString; 39 | return ',' + Object.keys(tags).map(key => key + '=' + tags[key].replace(/[:|,]/g, '-')).join(','); 40 | } 41 | 42 | gauge(metric: string, customTags?: any, delta?: number) { 43 | const tags = this.tags + this.encodeTags(customTags); 44 | this.statsd && this.statsd.increment(metric.toLowerCase() + tags, delta); 45 | } 46 | 47 | count(metric: string, customTags?: any, delta?: number) { 48 | const tags = this.tags + this.encodeTags(customTags); 49 | this.statsd && this.statsd.increment(metric.toLowerCase() + tags, delta); 50 | } 51 | 52 | timing(metric: string, duration: number, customTags?: any) { 53 | const tags = this.tags + this.encodeTags(customTags); 54 | this.statsd && this.statsd.timing(metric.toLowerCase() + tags, duration); 55 | } 56 | }*/ 57 | -------------------------------------------------------------------------------- /src/instrumentations/trackers/JaegerInstrumentation.ts: -------------------------------------------------------------------------------- 1 | import * as jaeger from 'jaeger-client'; 2 | const UDPSender = require('jaeger-client/dist/src/reporters/udp_sender').default; 3 | import * as opentracing from 'opentracing'; 4 | import { DynamicConfiguration } from '../../configurations/dynamicConfiguration'; 5 | import { ITrackerAdapter, IRequestTrackerFactory } from './index'; 6 | import { IRequestContext } from "../../pipeline/common"; 7 | import { TrackerId, SpanKind, ISpanTracker } from '../../instrumentations/common'; 8 | import { Service } from '../../globals/system'; 9 | import * as URL from 'url'; 10 | 11 | export class JaegerInstrumentation implements IRequestTrackerFactory { 12 | 13 | static create() { 14 | let jaegerAddress = DynamicConfiguration.getPropertyValue("jaeger"); 15 | if (jaegerAddress) { 16 | if (!jaegerAddress.startsWith("http://")) { 17 | jaegerAddress = "http://" + jaegerAddress; 18 | } 19 | if (!/:[0-9]+/.test(jaegerAddress)) { 20 | jaegerAddress = jaegerAddress + ':6832'; 21 | } 22 | 23 | let url = URL.parse(jaegerAddress); 24 | const sender = new UDPSender({host:url.hostname, port: url.port}); 25 | const tracer = new jaeger.Tracer(Service.fullServiceName, 26 | new jaeger.RemoteReporter(sender), 27 | new jaeger.RateLimitingSampler(1)); 28 | 29 | Service.log.info(null, () => `Enabling Jaeger instrumentation at ${jaegerAddress}`); 30 | 31 | return new JaegerInstrumentation(tracer); 32 | } 33 | return null; 34 | } 35 | 36 | constructor(private tracer) { 37 | } 38 | 39 | startSpan(span: ISpanTracker, name: string, action: string): ITrackerAdapter { 40 | const parentId = (span.context.requestTracker && span.context.requestTracker.id) || null; 41 | const parent = (parentId && new jaeger.SpanContext(null, null, null, parentId.correlationId, parentId.spanId, parentId.parentId, 0x01)) || null; 42 | return new JaegerRequestTracker(this.tracer, span.id, span.kind, name, action, parent); 43 | } 44 | } 45 | 46 | export class JaegerRequestTracker implements ITrackerAdapter { 47 | private rootSpan; 48 | 49 | get context() { 50 | return this.rootSpan.context(); 51 | } 52 | 53 | constructor(tracer, id: TrackerId, kind: SpanKind, name: string, action: string, parent: any) { 54 | if (kind === SpanKind.Command) { 55 | this.rootSpan = tracer.startSpan(name + " " + action, {childOf: parent}); 56 | this.rootSpan.setTag("event", "cs"); 57 | } 58 | else if (kind === SpanKind.Event) { 59 | this.rootSpan = tracer.startSpan("Event " + action, { childOf: parent }); 60 | this.rootSpan.setTag("event", "sr"); 61 | } 62 | else if (kind === SpanKind.Task) { 63 | this.rootSpan = tracer.startSpan("Async " + action, { childOf: parent }); 64 | this.rootSpan.setTag("event", "sr"); 65 | } 66 | else if (kind === SpanKind.Request) { 67 | this.rootSpan = tracer.startSpan(action, { childOf: parent }); 68 | this.rootSpan.setTag("event", "sr"); 69 | } 70 | this.rootSpan._spanContext = new jaeger.SpanContext(null, null, null, id.correlationId, id.spanId, id.parentId, 0x01); 71 | } 72 | 73 | log(msg: string) { 74 | this.rootSpan.log({ message: msg }); 75 | } 76 | 77 | addTag(name: string, value: string) { 78 | this.rootSpan.setTag(name, value); 79 | } 80 | 81 | trackError(error: Error, msg: string) { 82 | this.rootSpan.setTag(opentracing.Tags.ERROR, true); 83 | this.rootSpan.setTag("message", error.message); 84 | this.rootSpan.setTag("stack", error.stack); 85 | this.rootSpan.setTag("event", "error"); 86 | this.log(msg || error.message); 87 | } 88 | 89 | finish() { 90 | this.rootSpan.finish(); 91 | } 92 | } -------------------------------------------------------------------------------- /src/instrumentations/trackers/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Request tracer interface 3 | */ 4 | import { ZipkinInstrumentation } from './zipkinInstrumentation'; 5 | import { IContainer } from '../../di/resolvers'; 6 | import { IRequestContext } from "../../pipeline/common"; 7 | import { TrackerId, SpanKind, ISpanTracker } from '../../instrumentations/common'; 8 | import { JaegerInstrumentation } from './JaegerInstrumentation'; 9 | 10 | export interface ITrackerAdapter { 11 | log(msg: string); 12 | trackError(error, msg?: string); 13 | addTag(name: string, value: string); 14 | finish(); 15 | } 16 | 17 | export interface IRequestTrackerFactory { 18 | startSpan( span: ISpanTracker, name: string, action: string): ITrackerAdapter; 19 | } 20 | 21 | export class TrackerFactory { 22 | static create(container: IContainer): IRequestTrackerFactory { 23 | return ZipkinInstrumentation.create() || 24 | /*ApplicationInsightsMetrics.create() || */ 25 | JaegerInstrumentation.create(); 26 | } 27 | } -------------------------------------------------------------------------------- /src/instrumentations/trackers/zipkinInstrumentation.ts: -------------------------------------------------------------------------------- 1 | import { Service } from '../../globals/system'; 2 | import { Conventions } from '../../utils/conventions'; 3 | import { DynamicConfiguration } from '../../configurations/dynamicConfiguration'; 4 | import { ITrackerAdapter, IRequestTrackerFactory } from './index'; 5 | import { RequestContext } from "../../pipeline/requestContext"; 6 | import * as os from 'os'; 7 | import { IRequestContext } from "../../pipeline/common"; 8 | import { TrackerId, SpanKind, ISpanTracker } from '../../instrumentations/common'; 9 | 10 | const { 11 | Annotation, 12 | HttpHeaders: Header, 13 | option: {Some, None}, 14 | TraceId, Tracer, ExplicitContext, ConsoleRecorder, BatchRecorder 15 | } = require('zipkin'); 16 | const {HttpLogger} = require('zipkin-transport-http'); 17 | 18 | /** 19 | * Needs a property named : zipkin 20 | */ 21 | export class ZipkinInstrumentation implements IRequestTrackerFactory { 22 | 23 | static create() { 24 | let zipkinAddress = DynamicConfiguration.getPropertyValue("zipkin"); 25 | if (zipkinAddress) { 26 | if (!zipkinAddress.startsWith("http://")) { 27 | zipkinAddress = "http://" + zipkinAddress; 28 | } 29 | if (!/:[0-9]+/.test(zipkinAddress)) { 30 | zipkinAddress = zipkinAddress + ':9411'; 31 | } 32 | 33 | Service.log.info(null, () => `Enabling Zipkin instrumentation at ${zipkinAddress}`); 34 | 35 | const recorder = new BatchRecorder({ 36 | logger: new HttpLogger({ 37 | endpoint: `${zipkinAddress}/api/v1/spans`, 38 | httpInterval: 10000 39 | }) 40 | }); 41 | return new ZipkinInstrumentation(recorder ); 42 | } 43 | return null; 44 | } 45 | 46 | constructor(private recorder) { 47 | } 48 | 49 | startSpan(span: ISpanTracker, name: string, action: string): ITrackerAdapter { 50 | return new ZipkinRequestTracker(this.recorder, span.id, span.kind, name, action); 51 | } 52 | } 53 | 54 | class ZipkinRequestTracker implements ITrackerAdapter { 55 | private tracer; 56 | private id: any; 57 | 58 | constructor(recorder, spanId: TrackerId, private kind: SpanKind, name: string, private action: string) { 59 | this.tracer = new Tracer({ ctxImpl: new ExplicitContext(), recorder }); 60 | 61 | this.id = new TraceId({ 62 | traceId: new Some(spanId.correlationId), 63 | spanId: spanId.spanId, 64 | parentId: spanId.parentId ? new Some(spanId.parentId) : None, 65 | Sampled: None, 66 | Flags: 0 67 | }); 68 | 69 | // console.log(`Start span ${name}, action ${action}, id: ${this.id}; kind: ${kind}`); 70 | 71 | this.tracer.setId(this.id); 72 | this.tracer.recordRpc(action); 73 | this.tracer.recordServiceName(name); 74 | this.tracer.recordLocalAddr(os.hostname()); 75 | 76 | if (kind === SpanKind.Command) 77 | this.tracer.recordAnnotation(new Annotation.ClientSend()); 78 | else if (kind === SpanKind.Event) 79 | this.tracer.recordAnnotation(new Annotation.ServerRecv()); 80 | else if (kind === SpanKind.Task) 81 | this.tracer.recordAnnotation(new Annotation.ServerRecv()); 82 | else if (kind === SpanKind.Request) 83 | this.tracer.recordAnnotation(new Annotation.ServerRecv()); 84 | } 85 | 86 | log(msg: string) { 87 | } 88 | 89 | addTag(name: string, value: string) { 90 | this.tracer.recordBinary(name, value.replace(/[:|,\.?&]/g, '-')); 91 | } 92 | 93 | trackError(error, msg: string) { 94 | this.tracer.recordBinary("error", error.message || error); 95 | } 96 | 97 | finish() { 98 | // console.log(`End span ${this.name}, action ${this.action}, id: ${this.id}; kind: ${this.kind}`); 99 | if (this.kind === SpanKind.Command) 100 | this.tracer.recordAnnotation(new Annotation.ClientRecv()); 101 | else if (this.kind === SpanKind.Event) 102 | this.tracer.recordAnnotation(new Annotation.ServerSend()); 103 | else if (this.kind === SpanKind.Task) 104 | this.tracer.recordAnnotation(new Annotation.ServerSend()); 105 | else if (this.kind === SpanKind.Request) 106 | this.tracer.recordAnnotation(new Annotation.ServerSend()); 107 | 108 | this.tracer = null; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/log/logger.ts: -------------------------------------------------------------------------------- 1 | import { EntryKind } from "./vulcainLogger"; 2 | import { IRequestContext } from "../pipeline/common"; 3 | 4 | export interface Logger { 5 | error(context: IRequestContext, error: Error, msg?: () => string); 6 | info(context: IRequestContext, msg: () => string); 7 | verbose(context: IRequestContext, msg: () => string): boolean; 8 | logAction(context: IRequestContext, kind: EntryKind, message?: string); 9 | } -------------------------------------------------------------------------------- /src/pipeline/common.ts: -------------------------------------------------------------------------------- 1 | import { UserContext } from '../security/securityContext'; 2 | import { IContainer } from '../di/resolvers'; 3 | import { ICommand } from "../commands/abstractCommand"; 4 | import { HttpRequest } from "./vulcainPipeline"; 5 | import { ISpanTracker, ITracker } from '../instrumentations/common'; 6 | import { Model } from '../schemas/builder/annotations.model'; 7 | import { Property} from '../schemas/builder/annotations.property'; 8 | 9 | export interface VulcainResponse { 10 | meta: { 11 | correlationId: string; 12 | taskId?: string; 13 | status?: string; 14 | totalCount?: number; 15 | page?: number; 16 | pageSize?: number; 17 | }; 18 | value: T; 19 | } 20 | export enum Pipeline { 21 | Event, 22 | AsyncTask, 23 | HttpRequest, 24 | Test 25 | } 26 | 27 | /** 28 | * Internal use 29 | * 30 | * @export 31 | * @interface ICustomEvent 32 | */ 33 | export interface ICustomEvent { 34 | action: string; 35 | schema?: string; 36 | params?: any; 37 | } 38 | 39 | export interface IRequestContext { 40 | createCustomTracker(name: string, tags?: { [index: string]: string }): ITracker; 41 | 42 | /** 43 | * Span tracker 44 | */ 45 | requestTracker: ITracker; 46 | 47 | /** 48 | * Current user or null 49 | * 50 | * @type {UserContext} 51 | */ 52 | user: UserContext; 53 | 54 | /** 55 | * Scoped container 56 | * 57 | * @type {IContainer} 58 | */ 59 | container: IContainer; 60 | 61 | /** 62 | * Current locale 63 | * 64 | * @type {string} 65 | * @memberOf RequestContext 66 | */ 67 | locale: string; 68 | 69 | /** 70 | * Request host name 71 | * 72 | * @type {string} 73 | * @memberOf RequestContext 74 | */ 75 | hostName: string; 76 | requestData: RequestData; 77 | request?: HttpRequest; 78 | parent: IRequestContext; 79 | /** 80 | * Send custom event from current service 81 | * 82 | * @param {string} action action event 83 | * @param {*} [params] action parameters 84 | * @param {string} [schema] optional schema 85 | */ 86 | sendCustomEvent(action: string, params?: any, schema?: string); 87 | 88 | /** 89 | * Log an error 90 | * 91 | * @param {Error} error Error instance 92 | * @param {string} [msg] Additional message 93 | * 94 | */ 95 | logError(error: Error, msg?: () => string); 96 | 97 | /** 98 | * Log a message info 99 | * 100 | * @param {string} msg Message format (can include %s, %j ...) 101 | * @param {...Array} params Message parameters 102 | * 103 | */ 104 | logInfo(msg: () => string); 105 | 106 | /** 107 | * Log a verbose message. Verbose message are enable by service configuration property : enableVerboseLog 108 | * 109 | * @param {any} context Current context 110 | * @param {string} msg Message format (can include %s, %j ...) 111 | * @param {...Array} params Message parameters 112 | * 113 | */ 114 | logVerbose(msg: () => string); 115 | 116 | /** 117 | * Don't close the request (used by SSE request) 118 | */ 119 | keepConnected: boolean; 120 | 121 | dispose(); 122 | } 123 | 124 | export interface RequestData { 125 | vulcainVerb: string; 126 | correlationId: string; 127 | action: string; 128 | domain: string; 129 | schema: string; 130 | params?: any; 131 | pageSize?: number; 132 | page?: number; 133 | inputSchema?: string; 134 | body?: any; 135 | } 136 | -------------------------------------------------------------------------------- /src/pipeline/errors/applicationRequestError.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 4 | * 5 | * @export 6 | * @class ApplicationError 7 | * @extends {Error} 8 | */ 9 | export class ApplicationError extends Error { 10 | public errors: {[propertyName: string]: string}|undefined; 11 | public messageTitle?: string; 12 | 13 | /** 14 | * Creates an instance of ApplicationRequestError. 15 | * 16 | * @param {ErrorResponse} error 17 | */ 18 | constructor(public message: string, public statusCode = 500, errors?: { [propertyName: string]: string }) { 19 | super(); 20 | this.errors = errors; 21 | this.messageTitle = message; 22 | } 23 | } 24 | 25 | /** 26 | * Fordidden error 27 | * 28 | * @export 29 | * @class ForbiddenRequestError 30 | * @extends {ApplicationRequestError} 31 | */ 32 | export class UnauthorizedRequestError extends ApplicationError { 33 | constructor(msg = "Unauthorized") { 34 | super(msg, 401); 35 | } 36 | } 37 | 38 | /** 39 | * 40 | */ 41 | export class ForbiddenRequestError extends ApplicationError { 42 | constructor(msg = "Forbidden") { 43 | super(msg, 403); 44 | } 45 | } 46 | 47 | export class NotFoundError extends ApplicationError { 48 | constructor(msg = "Not found") { 49 | super(msg, 404); 50 | } 51 | } -------------------------------------------------------------------------------- /src/pipeline/errors/badRequestError.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationError } from './applicationRequestError'; 2 | // Error but not count as an exception in the metrics, has no incidence on circuit breaker 3 | // and do not call getfallback 4 | export class BadRequestError extends ApplicationError { 5 | constructor(message: string, errors?: { [propertyName: string]: string }) { 6 | super(message, 400, errors); 7 | 8 | let msg = this.message; 9 | if (this.errors) { 10 | msg += " ["; 11 | let first = true; 12 | Object.keys(this.errors).forEach( 13 | (k) => { 14 | if (!first) 15 | msg = msg + ", "; 16 | first = false; 17 | msg = msg + `${k}: ${this.errors[k]}`; 18 | }); 19 | msg += "]"; 20 | this.message = msg; 21 | } 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/pipeline/errors/commandRuntimeError.ts: -------------------------------------------------------------------------------- 1 | import { FailureType } from '../../commands/executionResult'; 2 | import { ApplicationError } from './applicationRequestError'; 3 | 4 | export class CommandRuntimeError extends ApplicationError { 5 | constructor(public failureType: FailureType, public commandName: string, message: string, public error?: Error) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/pipeline/errors/runtimeError.ts: -------------------------------------------------------------------------------- 1 | export class RuntimeError extends Error { 2 | constructor(message: string) { 3 | super(message); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/pipeline/errors/timeoutError.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationError } from "./applicationRequestError"; 2 | 3 | export class TimeoutError extends ApplicationError { 4 | constructor(ms:number) { 5 | super("Timeout error - Command take more than " + ms + "ms"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/pipeline/handlerProcessor.ts: -------------------------------------------------------------------------------- 1 | import { DefaultServiceNames, Inject } from '../di/annotations'; 2 | import { IContainer } from "../di/resolvers"; 3 | import { Service } from "../globals/system"; 4 | import { IRequestContext, RequestData } from "./common"; 5 | import { UnauthorizedRequestError } from "./errors/applicationRequestError"; 6 | import { CommandManager } from "./handlers/action/actionManager"; 7 | import { ActionDefinition } from "./handlers/action/definitions"; 8 | import { IManager } from "./handlers/definitions"; 9 | import { Handler, ServiceDescriptors } from "./handlers/descriptions/serviceDescriptions"; 10 | import { QueryManager } from "./handlers/query/queryManager"; 11 | import { ContextWrapper, RequestContext } from "./requestContext"; 12 | import { HttpResponse } from "./response"; 13 | 14 | export class HandlerProcessor { 15 | private actionManager: CommandManager; 16 | private queryManager: QueryManager; 17 | private _serviceDescriptors: ServiceDescriptors; 18 | 19 | constructor(@Inject(DefaultServiceNames.Container) private container: IContainer) { 20 | this.actionManager = new CommandManager(container); 21 | this.queryManager = new QueryManager(container); 22 | } 23 | 24 | public async invokeHandler(context: IRequestContext, info: Handler, contextData?: RequestData) { 25 | // Verify authorization 26 | if (!context.user.hasScope(info.definition.scope)) { 27 | context.logError(new Error(`Unauthorized for handler ${info.verb} with scope=${info.definition.scope}`), () => `Current user is user=${ctx.user.name}, scopes=${ctx.user.scopes}`); 28 | throw new UnauthorizedRequestError(); 29 | } 30 | 31 | let ctx = contextData ? new ContextWrapper(context, contextData) : context; 32 | 33 | let command = ctx.requestData; 34 | let result: HttpResponse; 35 | const stubs = Service.getStubManager(ctx.container); 36 | let params = Object.assign({}, command.params || {}); 37 | let metadata = info.definition; 38 | 39 | result = stubs.enabled && await stubs.tryGetMockValue(ctx, metadata, info.verb, params); 40 | if (!stubs.enabled || result === undefined) { 41 | let manager: IManager; 42 | if (info.kind === "action") { 43 | manager = this.actionManager; 44 | } 45 | 46 | if (info.kind === "query") { 47 | manager = this.queryManager; 48 | } 49 | result = await manager.run(info, command, ctx); 50 | (ctx).response = result; 51 | } 52 | else { 53 | (ctx).response = result; 54 | stubs.enabled && await stubs.saveStub(ctx, metadata, info.verb, params, result); 55 | } 56 | 57 | return result; 58 | } 59 | 60 | public getHandlerInfo(container: IContainer, schema: string, action: string) { 61 | if (!this._serviceDescriptors) { 62 | this._serviceDescriptors = container.get(DefaultServiceNames.ServiceDescriptors); 63 | } 64 | let info = this._serviceDescriptors.getHandlerInfo(container, schema, action); 65 | return info; 66 | } 67 | } -------------------------------------------------------------------------------- /src/pipeline/handlers/abstractHandlers.ts: -------------------------------------------------------------------------------- 1 | import { IContainer } from '../../di/resolvers'; 2 | import { Inject, IScopedComponent } from '../../di/annotations'; 3 | import 'reflect-metadata'; 4 | import { RequestContext } from "../../pipeline/requestContext"; 5 | import { Pipeline, IRequestContext } from "../../pipeline/common"; 6 | import { EventNotificationMode, EventData } from "../../bus/messageBus"; 7 | const symMetadata = Symbol.for("handler:metadata"); 8 | 9 | export interface IActionMetadata { 10 | eventMode?: EventNotificationMode; 11 | action: string; 12 | scope?: string; 13 | schema: string; 14 | inputSchema?: string; 15 | metadata?: any; 16 | } 17 | 18 | export abstract class AbstractHandler implements IScopedComponent { 19 | private _requestContext: RequestContext; 20 | 21 | constructor( @Inject("Container") public container: IContainer) { 22 | } 23 | 24 | get context(): IRequestContext { 25 | if (!this._requestContext) { 26 | this._requestContext = new RequestContext(this.container, Pipeline.HttpRequest); 27 | } 28 | return this._requestContext; 29 | } 30 | 31 | set context(ctx: IRequestContext) { 32 | this._requestContext = ctx; 33 | } 34 | 35 | get metadata(): IActionMetadata { 36 | return Reflect.getMetadata(symMetadata, this.constructor); 37 | } 38 | } 39 | 40 | export abstract class AbstractActionHandler extends AbstractHandler { 41 | protected defineCommand?(metadata); 42 | protected createDefaultCommand?(); 43 | } 44 | 45 | export abstract class AbstractEventHandler extends AbstractHandler { 46 | event: EventData; 47 | } 48 | 49 | export abstract class AbstractQueryHandler extends AbstractHandler { 50 | } 51 | -------------------------------------------------------------------------------- /src/pipeline/handlers/action/definitions.ts: -------------------------------------------------------------------------------- 1 | import { MessageBus, EventNotificationMode, ConsumeEventDefinition, EventData } from '../../../bus/messageBus'; 2 | import { IContainer } from '../../../di/resolvers'; 3 | import { Domain } from '../../../schemas/domain'; 4 | import { DefaultServiceNames } from '../../../di/annotations'; 5 | import { ServiceDescriptors, Handler } from '../descriptions/serviceDescriptions'; 6 | import { Service } from '../../../globals/system'; 7 | import { RequestContext } from "../../../pipeline/requestContext"; 8 | import { RequestData, Pipeline, ICustomEvent } from "../../../pipeline/common"; 9 | import { CommandRuntimeError } from "../../errors/commandRuntimeError"; 10 | import { UserContextData } from "../../../security/securityContext"; 11 | import { HttpResponse } from "../../response"; 12 | import { ApplicationError } from "../../errors/applicationRequestError"; 13 | import { BadRequestError } from "../../errors/badRequestError"; 14 | import { ITaskManager } from "../../../providers/taskManager"; 15 | import { IRequestContext } from '../../../index'; 16 | import { HandlerProcessor } from '../../handlerProcessor'; 17 | import { EventHandlerFactory } from './eventHandlerFactory'; 18 | import { HandlerDefinition, OperationDefinition } from '../definitions'; 19 | 20 | export interface ExposeEventDefinition { 21 | schema?: string; 22 | mode?: EventNotificationMode; 23 | factory?: (context: IRequestContext, event: EventData) => EventData; 24 | options?: any; 25 | } 26 | 27 | /** 28 | * Declare default action handler definition 29 | * 30 | * @export 31 | * @interface ActionHandlerMetadata 32 | * @extends {HandlerDefinition} 33 | */ 34 | export interface ActionHandlerDefinition extends HandlerDefinition { 35 | /** 36 | * 37 | * 38 | * @type {boolean} 39 | * @memberOf ActionHandlerMetadata 40 | */ 41 | async?: boolean; 42 | /** 43 | * 44 | * 45 | * @type {EventNotificationMode} 46 | * @memberOf ActionHandlerMetadata 47 | */ 48 | eventMode?: EventNotificationMode; 49 | } 50 | 51 | /** 52 | * 53 | * 54 | * @export 55 | * @interface ActionMetadata 56 | * @extends {CommonActionMetadata} 57 | */ 58 | export interface ActionDefinition extends OperationDefinition { 59 | skipDataValidation?: boolean; 60 | async?: boolean; 61 | eventDefinition?: ExposeEventDefinition; 62 | } 63 | -------------------------------------------------------------------------------- /src/pipeline/handlers/definitions.ts: -------------------------------------------------------------------------------- 1 | import {IContainer} from '../../di/resolvers'; 2 | import {LifeTime} from '../../di/annotations'; 3 | import {Domain} from '../../schemas/domain'; 4 | import { RequestData } from "../../pipeline/common"; 5 | import { HttpResponse } from "../response"; 6 | import { RequestContext } from '../requestContext'; 7 | import { Schema } from '../../index'; 8 | import { Handler } from './descriptions/serviceDescriptions'; 9 | 10 | export interface OperationDefinition { 11 | description: string; 12 | name?: string; 13 | scope?: string; 14 | schema?: string; 15 | inputSchema?: string; 16 | outputSchema?: string; 17 | outputCardinality?: "one" | "many"; 18 | metadata?: any; 19 | } 20 | 21 | export interface HandlerDefinition { 22 | description?: string; 23 | schema?: string; 24 | metadata?: any; 25 | scope: string; 26 | serviceName?: string; 27 | serviceLifeTime?: LifeTime; 28 | enableOnTestOnly?: boolean; 29 | } 30 | 31 | export interface IManager { 32 | container: IContainer; 33 | run(info: Handler, command: RequestData, ctx: RequestContext): Promise; 34 | } 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/pipeline/handlers/descriptions/operationDescription.ts: -------------------------------------------------------------------------------- 1 | export class OperationDescription { 2 | schema: string; 3 | kind: "action" | "query" | "get"; 4 | description: string; 5 | name: string; 6 | scope: string; 7 | inputSchema: string; 8 | outputSchema: string; 9 | outputCardinality?: string; 10 | verb: string; 11 | async: boolean; 12 | metadata: any; 13 | } 14 | -------------------------------------------------------------------------------- /src/pipeline/handlers/descriptions/propertyDescription.ts: -------------------------------------------------------------------------------- 1 | export class PropertyDescription { 2 | name: string; 3 | required: boolean; 4 | description: string; 5 | type: string; 6 | typeDescription: string; 7 | definition: any; 8 | order: number; 9 | reference?: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/pipeline/handlers/descriptions/schemaDescription.ts: -------------------------------------------------------------------------------- 1 | import { PropertyDescription } from "./propertyDescription"; 2 | 3 | export class SchemaDescription { 4 | name: string; 5 | idProperty: string; 6 | properties: Array; 7 | dependencies: Set; 8 | metadata?: any; 9 | extends?: string; 10 | } -------------------------------------------------------------------------------- /src/pipeline/handlers/descriptions/serviceDescription.ts: -------------------------------------------------------------------------------- 1 | import { OperationDescription } from "./operationDescription"; 2 | import { SchemaDescription } from "./schemaDescription"; 3 | import { Model } from "../../../schemas/builder/annotations.model"; 4 | import { Metadata } from "../../../utils/reflector"; 5 | 6 | @Model() 7 | @Metadata("system", true) 8 | export class ServiceDescription { 9 | domain: string; 10 | serviceName: string; 11 | serviceVersion: string; 12 | alternateAddress?: string; 13 | services: Array; 14 | schemas: Array; 15 | hasAsyncTasks: boolean; 16 | scopes: Array<{ name: string, description: string }>; 17 | } -------------------------------------------------------------------------------- /src/pipeline/handlers/errorResponse.ts: -------------------------------------------------------------------------------- 1 | export interface ErrorResponse { 2 | message: string; 3 | errors?: { [propertyName: string]: string }; 4 | } -------------------------------------------------------------------------------- /src/pipeline/handlers/query/annotations.query.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { ServiceDescriptors } from '../descriptions/serviceDescriptions'; 3 | import { QueryOperationDefinition, QueryDefinition } from './definitions'; 4 | import { Preloader } from '../../../preloader'; 5 | import { Service } from '../../../globals/system'; 6 | import { IContainer } from '../../../di/resolvers'; 7 | import { DefaultServiceNames } from '../../../di/annotations'; 8 | import { Utils } from '../utils'; 9 | import { ApplicationError } from '../../../pipeline/errors/applicationRequestError'; 10 | 11 | //const symMetadata = Symbol.for("handler:metadata"); 12 | const symActions = Symbol.for("handler:actions"); 13 | const symMetadata = Symbol.for("handler:metadata"); 14 | 15 | /** 16 | * Define a query handler 17 | * 18 | * @export 19 | * @param {QueryActionMetadata} [def] 20 | * @returns 21 | */ 22 | export function Query(def: QueryOperationDefinition, metadata?:any) { 23 | return (target, key) => { 24 | let actions: { [name: string]: QueryOperationDefinition } = Reflect.getOwnMetadata(symActions, target.constructor) || {}; 25 | actions[key] = { ...actions[key], ...def }; 26 | actions[key].metadata = metadata || {}; 27 | 28 | if (!actions[key].inputSchema) { 29 | let params = Reflect.getMetadata("design:paramtypes", target, key); 30 | if (params && params.length > 0 && params[0].name !== "Object") { 31 | actions[key].inputSchema = Utils.resolveType(params[0]); 32 | } 33 | } 34 | let output = Reflect.getMetadata("design:returntype", target, key); 35 | if (output && ["Promise", "Object", "void 0", "null"].indexOf(output.name) < 0) { 36 | actions[key].outputSchema = Utils.resolveType(output.name); 37 | } 38 | if (!actions[key].name) { 39 | let tmp = key.toLowerCase(); 40 | if (tmp.endsWith("async")) tmp = tmp.substr(0, tmp.length - 5); 41 | actions[key].name = tmp; 42 | } 43 | 44 | if (!/^[_a-zA-Z][a-zA-Z0-9]*$/.test(actions[key].name)) { 45 | if (actions[key].name[0] !== '_' || !actions[key].metadata.system) // Only system handler can begin with _ (to be consistant withj graphql) 46 | throw new ApplicationError(`Query name ${actions[key].name}has invalid caracter. Must be '[a-zA-Z][a-zA-Z0-9]*'`); 47 | } 48 | 49 | Reflect.defineMetadata(symActions, actions, target.constructor); 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/pipeline/handlers/query/annotations.queryHandler.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { ServiceDescriptors } from '../descriptions/serviceDescriptions'; 3 | import { QueryOperationDefinition, QueryDefinition } from './definitions'; 4 | import { Preloader } from '../../../preloader'; 5 | import { Service } from '../../../globals/system'; 6 | import { IContainer } from '../../../di/resolvers'; 7 | import { DefaultServiceNames } from '../../../di/annotations'; 8 | import { Utils } from '../utils'; 9 | import { DefaultQueryHandler } from '../../../defaults/crudHandlers'; 10 | 11 | const symActions = Symbol.for("handler:actions"); 12 | const symMetadata = Symbol.for("handler:metadata"); 13 | 14 | /** 15 | * Define a query handler class 16 | * 17 | * @export 18 | * @param {QueryMetadata} def 19 | * @returns 20 | */ 21 | export function QueryHandler(def: QueryDefinition, metadata?: any) { 22 | return function (target: Function) { 23 | if (def.enableOnTestOnly && !Service.isTestEnvironment) 24 | return; 25 | def.scope = def.scope || "?"; 26 | def.metadata = metadata; 27 | 28 | Preloader.instance.registerHandler((container: IContainer, domain) => { 29 | const symModel = Symbol.for("design:model"); 30 | let modelMetadatas = Reflect.getOwnMetadata(symModel, target); 31 | if (modelMetadatas) { 32 | // QueryHandler targets a model 33 | def.schema = modelMetadatas.name || target.name; 34 | let newName = '$$' + target.name + 'QueryHandler'; 35 | target = class extends DefaultQueryHandler { }; 36 | Object.defineProperty(target, 'name', { value: newName, configurable: true }); 37 | } 38 | let descriptors = container.get(DefaultServiceNames.ServiceDescriptors); 39 | let actions = Utils.getMetadata(symActions, target); 40 | descriptors.register(container, domain, target, actions, def, "query"); 41 | // DefaultHandler 42 | target.prototype.defineCommand && target.prototype.defineCommand.call(null, def); 43 | Reflect.defineMetadata(symMetadata, def, target); 44 | }); 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/pipeline/handlers/query/definitions.ts: -------------------------------------------------------------------------------- 1 | import { OperationDefinition, HandlerDefinition } from "../definitions"; 2 | 3 | export interface QueryDefinition extends HandlerDefinition { 4 | } 5 | 6 | export interface QueryOperationDefinition extends OperationDefinition { 7 | } -------------------------------------------------------------------------------- /src/pipeline/handlers/query/queryManager.ts: -------------------------------------------------------------------------------- 1 | import { IContainer } from '../../../di/resolvers'; 2 | import { Domain } from '../../../schemas/domain'; 3 | import { DefaultServiceNames } from '../../../di/annotations'; 4 | import { Handler } from '../descriptions/serviceDescriptions'; 5 | import { VulcainLogger } from '../../../log/vulcainLogger'; 6 | import { RequestContext } from "../../../pipeline/requestContext"; 7 | import { RequestData } from "../../../pipeline/common"; 8 | import { CommandRuntimeError } from "../../errors/commandRuntimeError"; 9 | import { HttpResponse } from "../../response"; 10 | import { BadRequestError } from "../../errors/badRequestError"; 11 | import { QueryResult } from './queryResult'; 12 | import { IManager } from '../definitions'; 13 | import { Utils } from '../utils'; 14 | 15 | export class QueryManager implements IManager { 16 | private _domain: Domain; 17 | 18 | /** 19 | * Get the current domain model 20 | * @returns {Domain} 21 | */ 22 | get domain() { 23 | if (!this._domain) { 24 | this._domain = this.container.get(DefaultServiceNames.Domain); 25 | } 26 | return this._domain; 27 | } 28 | 29 | constructor(public container: IContainer) { 30 | } 31 | 32 | private validateRequestData(ctx: RequestContext, info: Handler, query) { 33 | let errors; 34 | let inputSchema = info.definition.inputSchema; 35 | if (inputSchema && inputSchema !== "none") { 36 | let schema = inputSchema && this.domain.getSchema(inputSchema); 37 | if (schema) { 38 | query.inputSchema = schema.name; 39 | 40 | // Custom binding if any 41 | query.params = schema.coerce(query.params); 42 | 43 | errors = schema.validate(ctx, query.params); 44 | } 45 | 46 | if (!errors) { 47 | // Search if a method naming validate[Async] exists 48 | let methodName = 'validate' + inputSchema; 49 | errors = info.handler[methodName] && info.handler[methodName](query.params, query.action); 50 | } 51 | } 52 | return errors; 53 | } 54 | 55 | async run(info: Handler, query: RequestData, ctx: RequestContext): Promise { 56 | 57 | let logger = this.container.get(DefaultServiceNames.Logger); 58 | 59 | try { 60 | let errors = this.validateRequestData(ctx, info, query); 61 | if (errors && Object.keys(errors).length > 0) { 62 | throw new BadRequestError("Validation errors", errors); 63 | } 64 | 65 | query.schema = query.schema || info.definition.schema; 66 | info.handler.context = ctx; 67 | 68 | let result = await info.handler[info.methodName](query.params); 69 | 70 | if (!(result instanceof HttpResponse)) { 71 | let values = result; 72 | let total = 0; 73 | if (result instanceof QueryResult) { 74 | values = result.value; 75 | total = result.totalCount; 76 | } 77 | 78 | let res:any = { meta: {}, value: Utils.obfuscateSensibleData(this.domain, this.container, values) }; 79 | res.meta.totalCount = total; 80 | if (result && Array.isArray(result)) { 81 | res.meta.totalCount = res.meta.totalCount || result.length; 82 | res.meta.pageSize = query.pageSize; 83 | res.meta.page = query.page; 84 | } 85 | return new HttpResponse(res); 86 | } 87 | return result; 88 | } 89 | catch (e) { 90 | let error = (e instanceof CommandRuntimeError && e.error) ? e.error : e; 91 | throw error; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/pipeline/handlers/query/queryResult.ts: -------------------------------------------------------------------------------- 1 | 2 | export class QueryResult { 3 | constructor(public value: Array, public totalCount?: number) { } 4 | } 5 | -------------------------------------------------------------------------------- /src/pipeline/handlers/utils.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from '../../schemas/schema'; 2 | import { Domain } from '../../schemas/domain'; 3 | import { IContainer } from '../../di/resolvers'; 4 | import { ServiceDescriptors } from './descriptions/serviceDescriptions'; 5 | 6 | export class Utils { 7 | 8 | // Get registered metadata by reverse hierarchy order 9 | // to override base metadata 10 | static getMetadata(key, target) { 11 | let metadata; 12 | if (target) { 13 | metadata = Utils.getMetadata(key, Object.getPrototypeOf(target)); 14 | let tmp = Reflect.getOwnMetadata(key, target); 15 | if (tmp) { 16 | // merge 17 | metadata = Object.assign(metadata, tmp); 18 | } 19 | } 20 | return metadata || {}; 21 | } 22 | static obfuscateSensibleData(domain: Domain, container: IContainer, result?: any) { 23 | if (result) { 24 | if (Array.isArray(result)) { 25 | let outputSchema: Schema | null; 26 | result.forEach(v => { 27 | if (v && v._schema) { 28 | if (!outputSchema || outputSchema.name !== v._schema) 29 | outputSchema = domain.getSchema(v._schema); 30 | if (outputSchema && outputSchema.info.hasSensibleData) 31 | outputSchema.obfuscate(v); 32 | } 33 | }); 34 | } 35 | else if (result._schema) { 36 | let outputSchema = domain.getSchema(result._schema); 37 | if (outputSchema && outputSchema.info.hasSensibleData) 38 | outputSchema.obfuscate(result); 39 | } 40 | } 41 | 42 | return result; 43 | } 44 | 45 | static resolveType(type): string { 46 | if (typeof type === "function" && ServiceDescriptors.nativeTypes.indexOf(type.name.toLowerCase()) >= 0) 47 | return type.name; 48 | return type; 49 | } 50 | } -------------------------------------------------------------------------------- /src/pipeline/middlewares/ServerSideEventMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { RequestContext } from "../requestContext"; 2 | import { DefaultServiceNames } from "../../di/annotations"; 3 | import { VulcainMiddleware } from "../vulcainPipeline"; 4 | import { ITenantPolicy } from "../policies/defaultTenantPolicy"; 5 | import { IServerAdapter} from '../serverAdapter'; 6 | 7 | export class ServerSideEventMiddleware { 8 | next: VulcainMiddleware; 9 | 10 | constructor(private adapter: IServerAdapter) { 11 | } 12 | 13 | invoke(ctx: RequestContext): Promise { 14 | let endpoint = this.adapter.getRoute(e => e.kind === "SSE" && e.verb === ctx.request.verb && ctx.request.url.pathname.startsWith(e.path)); 15 | if (endpoint) { 16 | ctx.logInfo(() => `SSE input : ${ctx.requestData.vulcainVerb}, params: ${JSON.stringify(ctx.requestData.params)}`); 17 | ctx.logInfo(() => `SSE context : user=${ctx.user.name}, scopes=${ctx.user.scopes}, tenant=${ctx.user.tenant}`); 18 | 19 | ctx.keepConnected = true; 20 | endpoint.handler(ctx); 21 | return; 22 | } 23 | 24 | if (this.next) 25 | return this.next.invoke(ctx); 26 | 27 | return; 28 | } 29 | } -------------------------------------------------------------------------------- /src/pipeline/middlewares/authenticationMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { RequestContext } from "../requestContext"; 2 | import { DefaultServiceNames } from "../../di/annotations"; 3 | import { VulcainMiddleware } from "../vulcainPipeline"; 4 | import { ITenantPolicy } from "../policies/defaultTenantPolicy"; 5 | 6 | export class AuthenticationMiddleware extends VulcainMiddleware { 7 | 8 | async invoke(ctx: RequestContext) { 9 | let tenantPolicy = ctx.container.get(DefaultServiceNames.TenantPolicy); 10 | ctx.setSecurityContext(tenantPolicy.resolveTenant(ctx)); 11 | 12 | await ctx.user.process(ctx); 13 | 14 | return await super.invoke(ctx); 15 | } 16 | } -------------------------------------------------------------------------------- /src/pipeline/middlewares/handlersMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { RequestContext } from "../requestContext"; 2 | import { VulcainMiddleware } from "../vulcainPipeline"; 3 | import { Service } from "../../globals/system"; 4 | import { IContainer } from "../../di/resolvers"; 5 | import { UnauthorizedRequestError, ApplicationError } from "../errors/applicationRequestError"; 6 | import { HttpResponse } from "../response"; 7 | import { Handler, ServiceDescriptors } from "../handlers/descriptions/serviceDescriptions"; 8 | import { RequestData } from "../common"; 9 | import { DefaultServiceNames } from '../../di/annotations'; 10 | import { HandlerProcessor } from "../handlerProcessor"; 11 | 12 | export class HandlersMiddleware extends VulcainMiddleware { 13 | private handlerProcessor: HandlerProcessor; 14 | 15 | constructor(private container: IContainer) { 16 | super(); 17 | this.handlerProcessor = this.container.get(DefaultServiceNames.HandlerProcessor); 18 | } 19 | 20 | async invoke(ctx: RequestContext) { 21 | let command = ctx.requestData; 22 | let info = this.handlerProcessor.getHandlerInfo(ctx.container, command.schema, command.action); 23 | 24 | // Check if handler exists 25 | if (!info) 26 | throw new ApplicationError(`no handler method founded for ${ctx.requestData.vulcainVerb}, path: ${ctx.request.nativeRequest.url}`, 405); 27 | 28 | let guard = false; 29 | if (ctx.request.verb === "POST" && info.kind === "action") { 30 | guard = true; 31 | } 32 | 33 | if (info.kind==="query" && (ctx.request.verb === "GET" || ctx.request.verb === "POST")) { 34 | guard = true; 35 | } 36 | 37 | if (info.kind === "action" && ctx.request.verb === "GET") 38 | throw new ApplicationError(`Action handler ${ctx.requestData.vulcainVerb} must be called with POST`, 405); 39 | 40 | if (!guard) { 41 | throw new ApplicationError(`Unsupported http verb for ${ctx.requestData.vulcainVerb}, path: ${ctx.request.nativeRequest.url}`, 405); 42 | } 43 | 44 | // Ensure schema name (casing) is valid 45 | ctx.requestData.schema = info.definition.schema || ctx.requestData.schema; 46 | 47 | ctx.logInfo(() => `Request input : ${ctx.requestData.vulcainVerb}, params: ${JSON.stringify(command.params)}`); 48 | ctx.logInfo(() => `Request context : user=${ctx.user.name}, scopes=${ctx.user.scopes}, tenant=${ctx.user.tenant}`); 49 | 50 | // Process handler 51 | await this.handlerProcessor.invokeHandler(ctx, info); 52 | return super.invoke(ctx); 53 | } 54 | } -------------------------------------------------------------------------------- /src/pipeline/middlewares/normalizeDataMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { RequestContext } from "../requestContext"; 2 | import { VulcainMiddleware } from "../vulcainPipeline"; 3 | import { BadRequestError } from "../errors/badRequestError"; 4 | import { Conventions } from "../../utils/conventions"; 5 | import { ApplicationError } from "../errors/applicationRequestError"; 6 | import { HttpResponse } from "../response"; 7 | import { Service } from "../../globals/system"; 8 | 9 | /** 10 | * Populate requestData property with action or query context 11 | */ 12 | export class NormalizeDataMiddleware extends VulcainMiddleware { 13 | 14 | // get 15 | // /api/customer.get[/id](?params) 16 | // /api/customer(?params) // action=all 17 | // post 18 | // /api/customer.create(?params) 19 | // params: 20 | // $action, _schema (force value) 21 | // $pageSize, $page 22 | async invoke(ctx: RequestContext) { 23 | try { 24 | ctx.normalize(); 25 | } 26 | catch (e) { 27 | ctx.logError(e, () => "Bad request format for " + ctx.request.url.pathname); 28 | ctx.response = HttpResponse.createFromError(new BadRequestError("Invalid request format")); 29 | return; 30 | } 31 | 32 | try { 33 | await super.invoke(ctx); 34 | if (!ctx.response) { 35 | ctx.response = new HttpResponse({}); 36 | } 37 | } 38 | catch (e) { 39 | if (!(e instanceof ApplicationError)) { 40 | e = new ApplicationError(e.message, 500); 41 | } 42 | if (!(e instanceof ApplicationError) || e.statusCode !== 405) { 43 | // Don't pollute logs with incorect request 44 | ctx.logError(e, () => "Request has error"); 45 | } 46 | ctx.response = HttpResponse.createFromError(e); 47 | } 48 | 49 | // Inject request context in response 50 | ctx.response.addHeader('Access-Control-Allow-Origin', '*'); // CORS 51 | 52 | if (Object.getOwnPropertyDescriptor(ctx.response.content, "value") || Object.getOwnPropertyDescriptor(ctx.response.content, "error")) { 53 | ctx.response.content.meta = ctx.response.content.meta || {}; 54 | ctx.response.content.meta.correlationId = ctx.requestData.correlationId; 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/pipeline/policies/defaultTenantPolicy.ts: -------------------------------------------------------------------------------- 1 | import { Service } from '../../globals/system'; 2 | import { RequestContext, VulcainHeaderNames } from "../../pipeline/requestContext"; 3 | 4 | export interface ITenantPolicy { 5 | resolveTenant(ctx: RequestContext); 6 | } 7 | 8 | /** 9 | * Default policy 10 | * 11 | * @export 12 | * @class DefaultPolicy 13 | */ 14 | export class DefaultTenantPolicy { 15 | 16 | protected resolveFromHeader(ctx: RequestContext): string { 17 | let tenant = ctx.request.headers[VulcainHeaderNames.X_VULCAIN_TENANT]; 18 | if (!tenant) 19 | return; 20 | 21 | if (tenant === "?") { 22 | // from load-balancer so resolve from hostname 23 | // Get the first sub-domain 24 | let pos = ctx.hostName.indexOf('.'); 25 | tenant = pos > 0 ? ctx.hostName.substr(0, pos) : ctx.hostName; 26 | // Remove port 27 | pos = tenant.indexOf(':'); 28 | if (pos > 0) { 29 | tenant = tenant.substr(0, pos); 30 | } 31 | return tenant; 32 | } 33 | 34 | if (!tenant.startsWith("pattern:")) { 35 | return tenant; 36 | } 37 | 38 | let pattern = tenant.substr("pattern:".length); 39 | try { 40 | const regex = new RegExp(pattern.trim()); 41 | const groups = regex.exec(ctx.hostName); 42 | if (groups && groups.length > 0) { 43 | return groups[1]; 44 | } 45 | } 46 | catch (e) { 47 | ctx.logError(e, ()=> "TENANT pattern cannot be resolved " + pattern); 48 | } 49 | } 50 | 51 | resolveTenant(ctx: RequestContext): string { 52 | let tenant: string; 53 | // 1 - tenant in url (test only) 54 | tenant = (Service.isTestEnvironment && ctx.requestData.params && ctx.requestData.params.$tenant); 55 | if (tenant) { 56 | return tenant; 57 | } 58 | 59 | // 2 - Header 60 | tenant = this.resolveFromHeader(ctx); 61 | if (tenant) { 62 | return tenant; 63 | } 64 | 65 | // 3 - default 66 | return Service.defaultTenant; 67 | } 68 | } -------------------------------------------------------------------------------- /src/pipeline/response.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This class provide a way to customize the http response. 3 | * 4 | * @export 5 | * @class HttpResponse 6 | */ 7 | import { ApplicationError } from "./errors/applicationRequestError"; 8 | 9 | export class HttpResponse { 10 | /** 11 | * Http code (default is 200) 12 | * 13 | * @type {number} 14 | * @memberOf HttpResponse 15 | */ 16 | public statusCode: number; 17 | /** 18 | * List of response headers 19 | * 20 | * @type {any} 21 | * @memberOf HttpResponse 22 | */ 23 | public headers: any; 24 | /** 25 | * Define a specific ContentType 26 | * 27 | * @type {string} 28 | * @memberOf HttpResponse 29 | */ 30 | public contentType: string|undefined; 31 | /** 32 | * Response content 33 | * 34 | * @type {*} 35 | * @memberOf HttpResponse 36 | */ 37 | public content: any; 38 | 39 | /** 40 | * Content encoding (like binary, hex,...) 41 | * 42 | * @type {string} 43 | * @memberOf HttpResponse 44 | */ 45 | public encoding: string; 46 | 47 | /* static createFromResponse(data): HttpVulcainResponse { 48 | let res = new HttpVulcainResponse(data.content, data.statusCode); 49 | res.encoding = data.encoding; 50 | res.contentType = data.contentType; 51 | res.headers = data.headers; 52 | return res; 53 | } 54 | */ 55 | static createFromError(err: ApplicationError): HttpResponse { 56 | let res = new HttpResponse({ error: { message: err.messageTitle || err.message, errors: err.errors }}, err.statusCode|| 500); 57 | return res; 58 | } 59 | 60 | constructor(content?:any, statusCode = 200) { 61 | this.headers = {}; 62 | this.statusCode = statusCode; 63 | 64 | this.content = content; 65 | } 66 | 67 | /** 68 | * Add a custom header value to the response 69 | * 70 | * @param {string} name 71 | * @param {string} value 72 | */ 73 | addHeader(name: string, value: string) { 74 | this.headers[name] = value; 75 | } 76 | } 77 | 78 | export class HttpRedirectResponse extends HttpResponse { 79 | constructor(url: string) { 80 | super(); 81 | if (!url) 82 | throw new Error("Url is required"); 83 | this.statusCode = 302; 84 | this.addHeader("Location", url); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/pipeline/serializers/defaultSerializer.ts: -------------------------------------------------------------------------------- 1 | import { ISerializer } from "./serializer"; 2 | import { HttpResponse } from "../response"; 3 | import { IContainer, DefaultServiceNames, HttpRequest } from "../../index"; 4 | import { ApplicationError } from "../errors/applicationRequestError"; 5 | import { BadRequestError } from "../errors/badRequestError"; 6 | 7 | export class DefaultSerializer implements ISerializer { 8 | private serializer: ISerializer; 9 | 10 | constructor(container: IContainer) { 11 | this.serializer = container.get(DefaultServiceNames.Serializer, true); 12 | } 13 | 14 | deserialize(request: HttpRequest) { 15 | if (!request.body) 16 | return null; 17 | 18 | if (this.serializer) { 19 | let body = this.deserialize(request); 20 | if (!body) 21 | return body; 22 | } 23 | 24 | let body = request.body; 25 | if (request.headers["content-type"] === "application/json") 26 | body = JSON.parse(request.body); 27 | 28 | return body; 29 | } 30 | 31 | serialize(request: HttpRequest, response: HttpResponse) { 32 | if (!response.contentType) { 33 | if (this.serializer) { 34 | let resp = this.serialize(request, response); 35 | if (!resp) 36 | return resp; 37 | } 38 | 39 | if (typeof response.content === "string") { 40 | response.contentType = "text/plain"; 41 | } 42 | else { 43 | response.contentType = "application/json"; 44 | response.encoding = response.encoding || "utf8"; 45 | response.content = JSON.stringify(response.content); 46 | } 47 | } 48 | return response; 49 | } 50 | } -------------------------------------------------------------------------------- /src/pipeline/serializers/serializer.ts: -------------------------------------------------------------------------------- 1 | import { HttpResponse } from "../response"; 2 | import { HttpRequest } from "../vulcainPipeline"; 3 | 4 | export interface ISerializer { 5 | serialize(request: HttpRequest, response: HttpResponse): HttpResponse; 6 | deserialize(request: HttpRequest): any; 7 | } -------------------------------------------------------------------------------- /src/pipeline/testContext.ts: -------------------------------------------------------------------------------- 1 | import { Service } from '../globals/system'; 2 | import { Domain } from '../schemas/domain'; 3 | import { Preloader } from '../preloader'; 4 | import { ConsoleMetrics } from "../instrumentations/metrics/consoleMetrics"; 5 | import { IContainer } from "../di/resolvers"; 6 | import { UserContext } from "../security/securityContext"; 7 | import { Container } from "../di/containers"; 8 | import { RequestContext } from "./requestContext"; 9 | import { Pipeline, IRequestContext } from "./common"; 10 | import { AbstractHandler } from "./handlers/abstractHandlers"; 11 | import { DefaultServiceNames } from '../di/annotations'; 12 | 13 | export class TestContext extends RequestContext { 14 | get rootContainer() { 15 | return this.container; 16 | } 17 | 18 | constructor() { 19 | super(new Container(), Pipeline.Test); 20 | let domain = new Domain(Service.domainName, this.container); 21 | this.container.injectInstance(domain, DefaultServiceNames.Domain); 22 | this.container.injectInstance(new ConsoleMetrics(), DefaultServiceNames.Metrics); 23 | Preloader.instance.runPreloads(this.container, domain); 24 | } 25 | 26 | setUser(user: UserContext) { 27 | this.setSecurityContext(user); 28 | return this; 29 | } 30 | 31 | getService(name: string): T { 32 | return this.context.container.get(name); 33 | } 34 | 35 | get context() { 36 | return TestContext.newContext(this.container); 37 | } 38 | 39 | static newContext(container: IContainer, data?: any): IRequestContext { 40 | let ctx = new RequestContext(container, Pipeline.Test, data); 41 | ctx.setSecurityContext("test"); 42 | ctx.normalize(); 43 | return ctx; 44 | } 45 | 46 | createHandler(handler: Function): T { 47 | let ctx = this.context; 48 | let scopedContainer = new Container(this.container, ctx); 49 | let h = new (<(container: IContainer) => void>handler)(scopedContainer); 50 | h.context = ctx; 51 | return h; 52 | } 53 | } -------------------------------------------------------------------------------- /src/pipeline/vulcainPipeline.ts: -------------------------------------------------------------------------------- 1 | import { RequestContext } from "./requestContext"; 2 | import url = require('url'); 3 | import { IContainer } from "../di/resolvers"; 4 | import { Pipeline } from "./common"; 5 | import { HttpResponse } from "./response"; 6 | import http = require('http'); 7 | 8 | export abstract class VulcainMiddleware { 9 | next: VulcainMiddleware; 10 | 11 | invoke(ctx: RequestContext): Promise { 12 | if (this.next) 13 | return this.next.invoke(ctx); 14 | 15 | return Promise.resolve(); 16 | } 17 | } 18 | 19 | export interface HttpRequest { 20 | url: url.Url; 21 | headers: { [header: string]: string | string[] }; 22 | body: any; 23 | verb: string; 24 | nativeRequest?: http.IncomingMessage; 25 | nativeResponse?: http.ServerResponse; 26 | } 27 | 28 | export class VulcainPipeline { 29 | private first: VulcainMiddleware; 30 | private last: VulcainMiddleware; 31 | 32 | constructor(middlewares?: VulcainMiddleware[]) { 33 | if (middlewares) { 34 | middlewares.forEach(m => this.use(m)); 35 | } 36 | } 37 | 38 | use(middleware: VulcainMiddleware) { 39 | if (!this.first) { 40 | this.first = this.last = middleware; 41 | } 42 | else { 43 | this.last.next = middleware; 44 | this.last = middleware; 45 | } 46 | return this; 47 | } 48 | 49 | async process(container: IContainer, request: HttpRequest) { 50 | let ctx = new RequestContext(container, 51 | Pipeline.HttpRequest, 52 | request 53 | ); 54 | 55 | try { 56 | await this.first.invoke(ctx); 57 | } 58 | finally { 59 | ctx.dispose(); 60 | } 61 | 62 | return ctx.keepConnected ? null : ctx.response; 63 | } 64 | } -------------------------------------------------------------------------------- /src/pipeline/vulcainServer.ts: -------------------------------------------------------------------------------- 1 | import { IContainer } from '../di/resolvers'; 2 | import { Conventions } from '../utils/conventions'; 3 | import { DefaultServiceNames } from '../di/annotations'; 4 | import { IMetrics } from '../instrumentations/metrics'; 5 | import { Service } from "../globals/system"; 6 | import { VulcainPipeline } from "./vulcainPipeline"; 7 | import { NormalizeDataMiddleware } from "./middlewares/normalizeDataMiddleware"; 8 | import { AuthenticationMiddleware } from "./middlewares/authenticationMiddleware"; 9 | import { HandlersMiddleware } from "./middlewares/handlersMiddleware"; 10 | import { IServerAdapter, HttpAdapter } from './serverAdapter'; 11 | import { GraphQLAdapter } from '../graphql/graphQLAdapter'; 12 | import { ServerSideEventMiddleware } from './middlewares/ServerSideEventMiddleware'; 13 | 14 | export class VulcainServer { 15 | private metrics: IMetrics; 16 | public adapter: IServerAdapter; 17 | 18 | constructor(protected domainName: string, protected container: IContainer) { 19 | this.metrics = container.get(DefaultServiceNames.Metrics); 20 | 21 | this.adapter = this.container.get(DefaultServiceNames.ServerAdapter, true) || new HttpAdapter(); 22 | this.adapter.init(container, 23 | new VulcainPipeline([ 24 | new NormalizeDataMiddleware(), 25 | new AuthenticationMiddleware(), 26 | new ServerSideEventMiddleware(this.adapter), 27 | new HandlersMiddleware(container) 28 | ])); 29 | this.container.injectInstance(this.adapter, DefaultServiceNames.ServerAdapter); // Override current adapter 30 | } 31 | 32 | public start(port: number) { 33 | this.container.getCustomEndpoints().forEach(e => { 34 | this.adapter.registerRoute(e); 35 | }); 36 | 37 | this.adapter.registerRoute({ 38 | kind: "HTTP", verb: "GET", path: "/healthz", handler: (req, res) => { 39 | res.statusCode = 200; 40 | res.end(); 41 | } 42 | }); 43 | 44 | this.adapter.start(port, (err) => { 45 | if (err) { 46 | process.exit(1); 47 | } 48 | 49 | Service.log.info(null, () => 'Listening on port ' + port); 50 | }); 51 | } 52 | } -------------------------------------------------------------------------------- /src/preloader.ts: -------------------------------------------------------------------------------- 1 | import { IContainer } from './di/resolvers'; 2 | 3 | interface Item { 4 | callback: (container, domain) => void; 5 | } 6 | 7 | const Models = "models"; 8 | const Services = "services"; 9 | const Handlers = "handlers"; 10 | 11 | 12 | export class Preloader { 13 | private static _instance: Preloader; 14 | 15 | static get instance() { 16 | if (!Preloader._instance) { 17 | Preloader._instance = new Preloader(); 18 | } 19 | return Preloader._instance; 20 | } 21 | 22 | private _preloads: { [name: string]: Array } = {}; 23 | 24 | registerModel(callback: (container, domain) => void) { 25 | this.register(Models, callback); 26 | } 27 | 28 | registerService(callback: (container, domain) => void) { 29 | this.register(Services, callback); 30 | } 31 | 32 | registerHandler(callback: (container, domain) => void) { 33 | this.register(Handlers, callback); 34 | } 35 | 36 | private register(key: string, callback) { 37 | let list = this._preloads[key]; 38 | if (!list) { 39 | this._preloads[key] = list = []; 40 | } 41 | list.push({ callback: callback }); 42 | } 43 | 44 | private run(key: string, container, domain) { 45 | let items = this._preloads[key]; 46 | if (!items) return; 47 | for (const item of items) { 48 | item.callback(container, domain); 49 | } 50 | } 51 | 52 | runPreloads(container: IContainer, domain) { 53 | if (this._preloads) { 54 | this.run(Models, container, domain); 55 | this.run(Services, container, domain); 56 | this.run(Handlers, container, domain); 57 | this._preloads = {}; 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/providers/memory/providerFactory.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, LifeTime, Inject } from '../../di/annotations'; 2 | import { DefaultServiceNames } from '../../di/annotations'; 3 | import { Schema } from '../../schemas/schema'; 4 | import { IProvider } from '../provider'; 5 | import { IContainer } from '../../di/resolvers'; 6 | import { Service } from '../../globals/system'; 7 | import { IRequestContext } from "../../pipeline/common"; 8 | import { MemoryProvider } from './provider'; 9 | 10 | interface PoolItem { 11 | provider?: MemoryProvider; 12 | count?: number; 13 | dispose?: () => void; 14 | } 15 | 16 | export class MemoryProviderFactory { 17 | private pool = new Map(); 18 | 19 | constructor(private dataFolder, public maxPoolSize = 20) { 20 | } 21 | 22 | private addToPool(context: IRequestContext, key: string, item: PoolItem) { 23 | Service.log.info(context, () => `Adding a new provider pool item : ${key}`); 24 | if (this.pool.size >= this.maxPoolSize) { 25 | // remove the least used 26 | let keyToRemove; 27 | let min = 0; 28 | for (const [key, value] of this.pool.entries()) { 29 | if (!keyToRemove || value.count < min) { 30 | keyToRemove = key; 31 | min = value.count; 32 | } 33 | } 34 | let item = this.pool.get(keyToRemove); 35 | item.dispose && item.dispose(); 36 | this.pool.delete(keyToRemove); 37 | Service.log.info(context, () => `Ejecting ${keyToRemove} from provider pool item.`); 38 | } 39 | item.count = 1; 40 | this.pool.set(key, item); 41 | } 42 | 43 | private getFromPool(key: string) { 44 | let item = this.pool.get(key); 45 | if (item) { 46 | item.count++; 47 | return item.provider; 48 | } 49 | } 50 | 51 | getConnection(context: IRequestContext, tenant: string): IProvider { 52 | tenant = tenant || context.user.tenant; 53 | let poolKey = tenant; 54 | let provider = this.getFromPool(poolKey); 55 | if (!provider) { 56 | provider = new MemoryProvider(this.dataFolder); 57 | let item: PoolItem = { provider }; 58 | item.dispose = provider.initialize(tenant); 59 | if (item.dispose) { 60 | this.addToPool(context, poolKey, item); 61 | } 62 | } 63 | 64 | return >provider; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/providers/provider.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Schema} from "../schemas/schema"; 3 | import { IRequestContext } from "../pipeline/common"; 4 | import { QueryResult } from "../index"; 5 | 6 | export interface QueryOptions { 7 | /** 8 | * Page size (default 20) 9 | */ 10 | pageSize?: number; // 0 for all 11 | /** 12 | * Page to returns 13 | * 14 | */ 15 | page?: number; 16 | /** 17 | * Optional filter 18 | */ 19 | query?: { 20 | filter?: any; 21 | projections?: any; 22 | sort?: any; 23 | }; 24 | } 25 | 26 | /** 27 | * Connection factory 28 | */ 29 | export interface IProviderFactory { 30 | /** 31 | * Create a connection from a pool 32 | * @param context Current context 33 | * @param tenant Tenant 34 | */ 35 | getConnection(context: IRequestContext, tenant: string): IProvider; 36 | } 37 | 38 | /** 39 | * Persistance provider for a schema 40 | */ 41 | export interface IProvider 42 | { 43 | /** 44 | * Server address 45 | */ 46 | address: string; 47 | 48 | /** 49 | * Get an entity list 50 | * 51 | * @param {IRequestContext} ctx Current context 52 | * @param {Schema} schema Entity schema 53 | * @param {QueryOptions} options 54 | * @returns {Promise>} 55 | * 56 | * @memberOf IProvider 57 | */ 58 | getAll(ctx: IRequestContext, schema: Schema, options: QueryOptions): Promise; 59 | /** 60 | * Get an entity by id 61 | * 62 | * @param {IRequestContext} ctx Current context 63 | * @param {Schema} schema Entity schema 64 | * @param {string} id 65 | * @returns {Promise} 66 | * 67 | * @memberOf IProvider 68 | */ 69 | get(ctx: IRequestContext, schema: Schema, id: string): Promise; 70 | /** 71 | * Create an entity 72 | * 73 | * @param {IRequestContext} ctx Current context 74 | * @param {Schema} schema Entity schema 75 | * @param {T} entity 76 | * @returns {Promise} The created entity 77 | * 78 | * @memberOf IProvider 79 | */ 80 | create(ctx: IRequestContext, schema: Schema, entity: T): Promise; 81 | /** 82 | * Update an entity 83 | * 84 | * @param {IRequestContext} ctx Current context 85 | * @param {Schema} schema Entity schema 86 | * @param {T} entity Entity to update 87 | * @returns {Promise} The updated entity 88 | * 89 | * @memberOf IProvider 90 | */ 91 | update(ctx: IRequestContext, schema: Schema, entity: T): Promise; 92 | /** 93 | * Delete an entity - Must returns the deleted entity or raise an error if id does not exist 94 | * 95 | * @param {IRequestContext} ctx Current context 96 | * @param {Schema} schema Entity schema 97 | * @param {(string)} id Id 98 | * @returns {Promise} Deleted entity 99 | */ 100 | delete(ctx: IRequestContext, schema: Schema, id: string ) : Promise; 101 | } 102 | 103 | -------------------------------------------------------------------------------- /src/providers/taskManager.ts: -------------------------------------------------------------------------------- 1 | import { AsyncTaskData } from "../pipeline/handlers/action/actionManager"; 2 | 3 | export interface ITaskManager { 4 | registerTask(task: AsyncTaskData): Promise; 5 | updateTask(task: AsyncTaskData): Promise; 6 | getTask(taskId: string): Promise; 7 | getAllTasks(query?: any): Promise; 8 | } -------------------------------------------------------------------------------- /src/schemas/builder/annotations.model.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { Domain } from '../domain'; 3 | import { IRequestContext } from "../../pipeline/common"; 4 | import { SchemaBuilder } from './schemaBuilder'; 5 | import { Preloader } from '../../preloader'; 6 | import { IContainer } from '../../di/resolvers'; 7 | 8 | /** 9 | * Model metadata definition 10 | */ 11 | export interface ModelDefinition { 12 | /** 13 | * Model name (default class name) 14 | */ 15 | name?: string; 16 | /** 17 | * Inherited type 18 | */ 19 | extends?: string; 20 | /** 21 | * Model description 22 | */ 23 | description?: string; 24 | /** 25 | * Coerce input data 26 | */ 27 | coerce?: ((data) => any) | boolean; 28 | /** 29 | * Validatation function 30 | */ 31 | validate?: (entity, ctx?: IRequestContext) => string; 32 | /** 33 | * Storage name (table or collection) - default = model name 34 | */ 35 | storageName?: string; 36 | /** 37 | * This model (or its children) has sensible data - Required if you want obfuscate sensible type data 38 | */ 39 | hasSensibleData?: boolean; 40 | inputModel?: boolean; 41 | } 42 | 43 | /** 44 | * Declare a data model 45 | */ 46 | export function Model(def?: ModelDefinition) { 47 | return function (target: Function) { 48 | def = def || {}; 49 | def.name = def.name || target.name; 50 | def.storageName = def.storageName || def.name; 51 | 52 | // Try to infer inherit type 53 | if (!def.extends) { 54 | let ext = Object.getPrototypeOf(target).name; 55 | if (ext) def.extends = ext; 56 | } 57 | const sym = Symbol.for("design:model"); 58 | Reflect.defineMetadata(sym, def, target); 59 | Preloader.instance.registerModel((container: IContainer, domain) => { 60 | SchemaBuilder.buildSchema(domain, def, target); 61 | }); 62 | }; 63 | } 64 | 65 | export function InputModel(def?: ModelDefinition) { 66 | def = def || {}; 67 | def.inputModel = true; 68 | return Model(def); 69 | } -------------------------------------------------------------------------------- /src/schemas/builder/annotations.ts: -------------------------------------------------------------------------------- 1 | import { Preloader } from '../../preloader'; 2 | import 'reflect-metadata'; 3 | import { Domain } from '../domain'; 4 | import { SchemaBuilder } from './schemaBuilder'; 5 | 6 | /** 7 | * Define a new type to use with model property 8 | * @param name Type name (default to class name) 9 | */ 10 | export function SchemaTypeDefinition(name?:string) { 11 | return function (target: any) { 12 | Domain.addType(name || target.name, new target.prototype.constructor()); 13 | }; 14 | } 15 | 16 | export function Validator(name: string, options?) { 17 | return (target, key) => { 18 | const symValidators = Symbol.for("design:validators"); 19 | let validators = Reflect.getOwnMetadata(symValidators, target, key) || []; 20 | validators.push({ name, options }); 21 | Reflect.defineMetadata(symValidators, validators, target, key); 22 | }; 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/schemas/schemaInfo.ts: -------------------------------------------------------------------------------- 1 | import { IRequestContext } from "../pipeline/common"; 2 | import { PropertyDefinition } from "./builder/annotations.property"; 3 | 4 | /** 5 | * Internal Property definition 6 | * 7 | */ 8 | export interface ModelPropertyDefinition extends PropertyDefinition { 9 | name: string; 10 | /** 11 | * List of validators - Do not use directly, use @Validator instead 12 | * 13 | * @type {Array} 14 | * @memberOf PropertyOptions 15 | */ 16 | validators?: Array; 17 | /** 18 | * Custom options for custom types 19 | * 20 | * @memberOf PropertyOptions 21 | */ 22 | metadata?: any; 23 | } 24 | 25 | export interface SchemaInfo { 26 | name: string; 27 | description?: string; 28 | properties: { [index: string]: ModelPropertyDefinition }; 29 | extends?: string; 30 | hasSensibleData?: boolean; 31 | coerce?: ((data) => any) | boolean; 32 | validate?: (val, ctx: IRequestContext) => string; 33 | storageName?: string; 34 | idProperty?: string; 35 | metadata?: any; 36 | isInputModel: boolean; 37 | } -------------------------------------------------------------------------------- /src/schemas/schemaType.ts: -------------------------------------------------------------------------------- 1 | import { IRequestContext } from "../pipeline/common"; 2 | 3 | export interface ISchemaValidation { 4 | description?: string; 5 | type?: string; 6 | validate?: (val: any, ctx: IRequestContext) => string; 7 | } 8 | 9 | export interface ISchemaTypeDefinition extends ISchemaValidation { 10 | scalarType?: string; 11 | name?: string; 12 | coerce?: (val: any) => any; 13 | } 14 | -------------------------------------------------------------------------------- /src/schemas/standards/alphanumeric.ts: -------------------------------------------------------------------------------- 1 | import { ISchemaTypeDefinition } from "../schemaType"; 2 | import { SchemaTypeDefinition } from "../builder/annotations"; 3 | const validator = require('validator'); 4 | 5 | @SchemaTypeDefinition("alphanumeric") 6 | export class Alphanumeric implements ISchemaTypeDefinition { 7 | description = "Must be an alphanumeric string"; 8 | type = "string"; 9 | message = "Property '{$propertyName}' must be an alphanumeric."; 10 | validate(val, ctx = { locale: 'en-US' }) { 11 | if (!validator.isAlphanumeric(val, ctx.locale)) 12 | return this.message; 13 | } 14 | } -------------------------------------------------------------------------------- /src/schemas/standards/arrayOf.ts: -------------------------------------------------------------------------------- 1 | import { ISchemaTypeDefinition } from "../schemaType"; 2 | import { SchemaTypeDefinition } from "../builder/annotations"; 3 | 4 | @SchemaTypeDefinition("arrayOf") 5 | export class ArrayOf implements ISchemaTypeDefinition { 6 | description= "Must be an array of ${items}"; 7 | $itemsType= null; 8 | messages= [ 9 | "Invalid value '{$value}' for '{$propertyName}', all values must be of type {$itemsType}.", 10 | "Invalid value '{$value}' for '{$propertyName}', value must be an array.", 11 | ]; 12 | validate(val) { 13 | if (!this.$itemsType) return "You must define array item type with the 'items' property."; 14 | if (!Array.isArray(val)) return this.messages[1]; 15 | let error = false; 16 | if (this.$itemsType !== "any") { 17 | val.forEach(e => { 18 | if (e && typeof e !== this.$itemsType) error = true; 19 | }); 20 | } 21 | if (error) return this.messages[0]; 22 | } 23 | } -------------------------------------------------------------------------------- /src/schemas/standards/boolean.ts: -------------------------------------------------------------------------------- 1 | import { ISchemaTypeDefinition } from "../schemaType"; 2 | import { SchemaTypeDefinition } from "../builder/annotations"; 3 | 4 | @SchemaTypeDefinition("boolean") 5 | export class Boolean implements ISchemaTypeDefinition { 6 | description = "Must be a boolean"; 7 | message = "Property '{$propertyName}' must be a boolean."; 8 | scalarType = "boolean"; 9 | coerce(val) { 10 | if (val === undefined || typeof val === "boolean") return val; 11 | return (typeof val === "string") ? val === "true" : !!val; 12 | } 13 | validate(val) { 14 | if (typeof val !== "boolean") return this.message; 15 | } 16 | } -------------------------------------------------------------------------------- /src/schemas/standards/date-iso8601.ts: -------------------------------------------------------------------------------- 1 | import { ISchemaTypeDefinition } from "../schemaType"; 2 | import { SchemaTypeDefinition } from "../builder/annotations"; 3 | const validator = require('validator'); 4 | 5 | @SchemaTypeDefinition("date-iso8601") 6 | export class DateIso8601 implements ISchemaTypeDefinition { 7 | description = "Must be an ISO8061 date"; 8 | type = "string"; 9 | message = "Property '{$propertyName}' must be an date on ISO8601 format."; 10 | validate(val) { 11 | if (!validator.isISO8601(val)) 12 | return this.message; 13 | } 14 | } -------------------------------------------------------------------------------- /src/schemas/standards/email.ts: -------------------------------------------------------------------------------- 1 | import { ISchemaTypeDefinition } from "../schemaType"; 2 | import { SchemaTypeDefinition } from "../builder/annotations"; 3 | const validator = require('validator'); 4 | 5 | @SchemaTypeDefinition("email") 6 | export class Email implements ISchemaTypeDefinition { 7 | description = "Must be an email"; 8 | message = "Property '{$propertyName}' must be an email."; 9 | type = "string"; 10 | validate(val) { 11 | if ((typeof val !== "string")) return this.message; 12 | 13 | if (!validator.isEmail(val)) 14 | return this.message; 15 | } 16 | } -------------------------------------------------------------------------------- /src/schemas/standards/enum.ts: -------------------------------------------------------------------------------- 1 | import { ISchemaTypeDefinition } from "../schemaType"; 2 | import { SchemaTypeDefinition } from "../builder/annotations"; 3 | 4 | @SchemaTypeDefinition("enum") 5 | export class Enumeration implements ISchemaTypeDefinition { 6 | description = "Must be one of [{$values}]"; 7 | type = "string"; 8 | $values: string[]; 9 | message = "Invalid property '{$propertyName}'. Must be one of [{$values}]."; 10 | validate(val) { 11 | if (!this.$values) return "You must define a list of valid values with the 'values' property."; 12 | if (this.$values.indexOf(val) === -1) return this.message; 13 | } 14 | } -------------------------------------------------------------------------------- /src/schemas/standards/id.ts: -------------------------------------------------------------------------------- 1 | import { ISchemaTypeDefinition } from "../schemaType"; 2 | import { SchemaTypeDefinition } from "../builder/annotations"; 3 | 4 | @SchemaTypeDefinition("id") 5 | export class ID implements ISchemaTypeDefinition { 6 | description= "Must be a string or a number"; 7 | message= "Property '{$propertyName}' must be a string or a number."; 8 | validate(val) { 9 | if (typeof val !== "string" && typeof val !== "number") return this.message; 10 | } 11 | } -------------------------------------------------------------------------------- /src/schemas/standards/integer.ts: -------------------------------------------------------------------------------- 1 | import { ISchemaTypeDefinition } from "../schemaType"; 2 | import { SchemaTypeDefinition } from "../builder/annotations"; 3 | 4 | @SchemaTypeDefinition("integer") 5 | export class Integer implements ISchemaTypeDefinition { 6 | description= "Must be an integer"; 7 | message = "Property '{$propertyName}' must be an integer."; 8 | scalarType = "number"; 9 | coerce(val) { 10 | if (val === undefined || typeof val === "number") return val; 11 | if (/^(\-|\+)?([0-9]+([0-9]+)?)$/.test(val)) 12 | return Number(val); 13 | return NaN; 14 | } 15 | validate(val) { 16 | if ((typeof val !== "number") || isNaN(val)) return this.message; 17 | } 18 | } -------------------------------------------------------------------------------- /src/schemas/standards/length.ts: -------------------------------------------------------------------------------- 1 | import { ISchemaTypeDefinition } from "../schemaType"; 2 | import { SchemaTypeDefinition } from "../builder/annotations"; 3 | 4 | @SchemaTypeDefinition("length") 5 | export class Length implements ISchemaTypeDefinition { 6 | description = "Must have a length between ${min} and ${max}"; 7 | type = "string"; 8 | $min: number; 9 | $max: number; 10 | messages = [ 11 | "Property '{$propertyName}' must have at least {$min} characters.", 12 | "Property '{$propertyName}' must have no more than {$max} characters." 13 | ]; 14 | validate(val) { 15 | let len = val.length; 16 | if (this.$min !== undefined) { 17 | if (len < this.$min) return this.messages[0]; 18 | } 19 | if (this.$max !== undefined) { 20 | if (len > this.$max) return this.messages[1]; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/schemas/standards/number.ts: -------------------------------------------------------------------------------- 1 | import { ISchemaTypeDefinition } from "../schemaType"; 2 | import { SchemaTypeDefinition } from "../builder/annotations"; 3 | 4 | @SchemaTypeDefinition("number") 5 | export class Number implements ISchemaTypeDefinition { 6 | description= "Must be a number."; 7 | message = "Property '{$propertyName}' must be a number."; 8 | scalarType = "number"; 9 | coerce(val) { 10 | if (val === undefined || typeof val === "number") return val; 11 | if (/^ (\-|\+)?([0 - 9] + (\.[0 - 9] +)?) $ /.test(val)) 12 | return parseFloat(val); 13 | return NaN; 14 | } 15 | validate(val) { 16 | if ((typeof val !== "number") || isNaN(val)) return this.message; 17 | } 18 | } -------------------------------------------------------------------------------- /src/schemas/standards/pattern.ts: -------------------------------------------------------------------------------- 1 | import { ISchemaTypeDefinition } from "../schemaType"; 2 | import { SchemaTypeDefinition } from "../builder/annotations"; 3 | 4 | @SchemaTypeDefinition("pattern") 5 | export class Pattern implements ISchemaTypeDefinition { 6 | description = "Must respect the regex expression {$pattern}"; 7 | $pattern = null; 8 | type = "string"; 9 | message = "Property '{$propertyName}' must match the following pattern : {$pattern}"; 10 | validate(val) { 11 | if (this.$pattern && new RegExp(this.$pattern).test(val) === false) return this.message; 12 | } 13 | } -------------------------------------------------------------------------------- /src/schemas/standards/range.ts: -------------------------------------------------------------------------------- 1 | import { ISchemaTypeDefinition } from "../schemaType"; 2 | import { SchemaTypeDefinition } from "../builder/annotations"; 3 | 4 | @SchemaTypeDefinition("range") 5 | export class Range implements ISchemaTypeDefinition { 6 | description = "Must be a number between {$min} and ${$max}"; 7 | type = "number"; 8 | $min = 0; 9 | $max = 1; 10 | message = "Invalid value '{$value}' for '{$propertyName}', value must be between {$min} and {$max}"; 11 | validate(val) { 12 | if (val < this.$min || val > this.$max) return this.message; 13 | } 14 | } -------------------------------------------------------------------------------- /src/schemas/standards/reference.ts: -------------------------------------------------------------------------------- 1 | import { ISchemaTypeDefinition } from "../schemaType"; 2 | import { SchemaTypeDefinition } from "../builder/annotations"; 3 | 4 | @SchemaTypeDefinition("$ref") 5 | export class Reference implements ISchemaTypeDefinition { 6 | $cardinality = "one"; 7 | $item = null; 8 | messages = [ 9 | "Collection is not allowed for the reference '{$propertyName}' with cardinality = one.", 10 | "Reference '{$propertyName}' with cardinality = many must contains an array.", 11 | "Reference element for property '{$propertyName}' must be of type {$item}." 12 | ]; 13 | validate(val) { 14 | if (this.$cardinality !== "one" && this.$cardinality !== "many") 15 | throw new Error("Incorrect cardinality. Allowed values are 'one' or 'many'"); 16 | if (this.$cardinality === "one") { 17 | if (Array.isArray(val)) return this.messages[0]; 18 | if (this.$item && val._schema && val._schema !== this.$item) return this.messages[2]; 19 | return; 20 | } 21 | if (this.$cardinality === "many") { 22 | if (!Array.isArray(val)) return this.messages[1]; 23 | if (this.$item && val) { 24 | let ok = true; 25 | val.forEach(v => { 26 | if (v._schema) ok = ok || v._schema === this.$item; 27 | }); 28 | if (!ok) return this.messages[2]; 29 | } 30 | return; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/schemas/standards/standards.ts: -------------------------------------------------------------------------------- 1 | const uuid = require('uuid'); 2 | const validator = require('validator'); 3 | 4 | export class TYPES { 5 | static "String" = "string"; 6 | static "Any" = "any"; 7 | static "Boolean" = "boolean"; 8 | static "Number" = "number"; 9 | static "Integer" = "integer"; 10 | static "Enum" = "enum"; 11 | static "Uid" = "uid"; 12 | static "ArrayOf" = "arrayOf"; 13 | static "Email" = "email"; 14 | static "Url" = "url"; 15 | static "Alphanumeric" = "alphanumeric"; 16 | static "Date-iso8601" = "date-iso8601"; 17 | } 18 | 19 | export class VALIDATORS { 20 | static "Range" = "range"; 21 | static "Pattern" = "pattern"; 22 | static "Length" = "length"; 23 | } 24 | -------------------------------------------------------------------------------- /src/schemas/standards/string.ts: -------------------------------------------------------------------------------- 1 | import { ISchemaTypeDefinition } from "../schemaType"; 2 | import { SchemaTypeDefinition } from "../builder/annotations"; 3 | 4 | @SchemaTypeDefinition("string") 5 | export class String implements ISchemaTypeDefinition { 6 | description= "Must be a string"; 7 | message = "Property '{$propertyName}' must be a string."; 8 | scalarType = "string"; 9 | validate(val) { 10 | if (typeof val !== "string") return this.message; 11 | } 12 | } -------------------------------------------------------------------------------- /src/schemas/standards/uid.ts: -------------------------------------------------------------------------------- 1 | import { ISchemaTypeDefinition } from "../schemaType"; 2 | import { SchemaTypeDefinition } from "../builder/annotations"; 3 | const uuid = require('uuid'); 4 | 5 | @SchemaTypeDefinition("uid") 6 | export class UID implements ISchemaTypeDefinition { 7 | description= "Must be an UID (will be generated if null)"; 8 | type= "id"; 9 | coerce(v) { 10 | return v || uuid.v1(); 11 | } 12 | } -------------------------------------------------------------------------------- /src/schemas/standards/url.ts: -------------------------------------------------------------------------------- 1 | import { ISchemaTypeDefinition } from "../schemaType"; 2 | import { SchemaTypeDefinition } from "../builder/annotations"; 3 | const validator = require('validator'); 4 | 5 | @SchemaTypeDefinition("url") 6 | export class Url implements ISchemaTypeDefinition { 7 | description = "Must be an url"; 8 | type = "string"; 9 | message = "Property '{$propertyName}' must be an url."; 10 | validate(val) { 11 | if (!validator.isURL(val)) 12 | return this.message; 13 | } 14 | } -------------------------------------------------------------------------------- /src/schemas/visitor.ts: -------------------------------------------------------------------------------- 1 | import { Domain } from "./domain"; 2 | import { Schema } from "./schema"; 3 | 4 | 5 | export interface IVisitor { 6 | visitEntity(obj, schema): boolean; 7 | visitProperty(val, schema); 8 | } 9 | 10 | export class SchemaVisitor { 11 | 12 | constructor(private domain:Domain, private visitor: IVisitor) { 13 | } 14 | 15 | visit(schema: Schema, entity) { 16 | 17 | if (this.visitor.visitEntity && !this.visitor.visitEntity(entity, schema)) 18 | return; 19 | 20 | let sch = schema; 21 | while (sch) { 22 | for (const ps in sch.info.properties) { 23 | if (!sch.info.properties.hasOwnProperty(ps)) continue; 24 | let prop = sch.info.properties[ps]; 25 | if (prop) { 26 | if (schema.isEmbeddedReference(prop)) { 27 | let refValue = entity[ps]; 28 | if (refValue) { 29 | let item = prop.type; 30 | if (refValue && refValue._schema) { 31 | item = refValue._schema; 32 | } 33 | let elemSchema = item && this.domain.getSchema(item, true); 34 | if (!elemSchema) { 35 | continue; 36 | } 37 | 38 | // elemSchema.name = ref; 39 | if (Array.isArray(refValue)) { 40 | for (let elem of refValue) { 41 | this.visit(elemSchema, elem); 42 | } 43 | } 44 | else { 45 | this.visit(elemSchema, refValue); 46 | } 47 | } 48 | } 49 | else { 50 | (prop).name = ps; 51 | let val = entity[ps]; 52 | this.visitor.visitProperty && this.visitor.visitProperty(val, prop); 53 | } 54 | } 55 | } 56 | sch = sch.extends; 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/security/authorizationPolicy.ts: -------------------------------------------------------------------------------- 1 | import { SecurityContext } from './securityContext'; 2 | import { Service } from "../globals/system"; 3 | 4 | export interface IAuthorizationPolicy { 5 | /** 6 | * get user scopes 7 | * 8 | * @returns {Array} 9 | * 10 | * @memberOf IPolicy 11 | */ 12 | scopes(sec: SecurityContext): Array; 13 | /** 14 | * Check if the current user scopes are valid with a specific scope 15 | * 16 | * @param {string} handlerScope Scope to check 17 | * @returns {boolean} 18 | * 19 | * @memberOf IPolicy 20 | */ 21 | hasScope(sec: SecurityContext, handlerScope: string): boolean; 22 | isAdmin(sec: SecurityContext): boolean; 23 | } 24 | 25 | /** 26 | * Default policy 27 | * 28 | * @export 29 | * @class DefaultPolicy 30 | */ 31 | export class DefaultAuthorizationPolicy { 32 | 33 | /** 34 | * Get user scopes 35 | * 36 | * @readonly 37 | * @type {Array} 38 | */ 39 | scopes(sec: SecurityContext): Array { 40 | return (sec && sec.scopes) || []; 41 | } 42 | 43 | /** 44 | * Check if the current user has a specific scope 45 | * 46 | * Rules: 47 | * scope userScope Result 48 | * null/?/* true 49 | * null false 50 | * * true 51 | * x x true 52 | * x-yz x-* true 53 | * 54 | * @param {string} scope 55 | * @returns {boolean} 56 | */ 57 | hasScope(sec: SecurityContext, handlerScope: string): boolean { 58 | if (!handlerScope || handlerScope === "?" || Service.isDevelopment) { 59 | return true; 60 | } 61 | if (!sec || !sec.name) { 62 | return false; 63 | } 64 | if (handlerScope === "*") { 65 | return true; 66 | } 67 | 68 | const handlerScopes = handlerScope.split(',').map(s => s.trim()); 69 | const userScopes = this.scopes(sec); 70 | 71 | if (!userScopes || userScopes.length === 0) { 72 | return false; 73 | } 74 | if (userScopes[0] === "*") { 75 | return true; 76 | } 77 | 78 | for (let userScope of userScopes) { 79 | let parts = userScope.split(':'); 80 | if (parts.length < 2) { 81 | continue; // malformed 82 | } 83 | 84 | if (parts[0] !== Service.domainName) { 85 | continue; 86 | } 87 | for (let sc of handlerScopes) { 88 | if (userScope === sc) { 89 | return true; 90 | } 91 | // admin:* means all scope beginning by admin: 92 | if (userScope.endsWith("*") && sc.startsWith(userScope.substr(0, userScope.length - 1))) { 93 | return true; 94 | } 95 | } 96 | } 97 | 98 | return false; 99 | } 100 | 101 | /** 102 | * Check if the current user is an admin 103 | * 104 | * @returns {boolean} 105 | */ 106 | isAdmin(sec: SecurityContext): boolean { 107 | let scopes = this.scopes(sec); 108 | return scopes && scopes.length > 0 && scopes[0] === "*"; 109 | } 110 | } -------------------------------------------------------------------------------- /src/stubs/istubManager.ts: -------------------------------------------------------------------------------- 1 | import { IRequestContext } from "../pipeline/common"; 2 | import { ActionDefinition } from "../pipeline/handlers/action/definitions"; 3 | import { HttpResponse } from "../pipeline/response"; 4 | 5 | export interface IStubManager { 6 | enabled: boolean; 7 | initialize?(sessions: any, saveHandler: Function); 8 | applyHttpStub(url: string, verb: string); 9 | applyServiceStub(serviceName: string, serviceVersion: string, verb: string, data); 10 | tryGetMockValue(ctx: IRequestContext, metadata: ActionDefinition, verb: string, params: any): Promise; 11 | saveStub(ctx: IRequestContext, metadata: ActionDefinition, verb: string, params: any, result: HttpResponse): Promise; 12 | } 13 | 14 | export class DummyStubManager implements IStubManager { 15 | get enabled() { return false;} 16 | tryGetMockValue(ctx: IRequestContext, metadata: ActionDefinition, verb: string, command: any): Promise { 17 | return Promise.resolve(null); 18 | } 19 | saveStub(ctx: IRequestContext, metadata: ActionDefinition, verb: string, command: any, result: HttpResponse): Promise { 20 | return Promise.resolve(); 21 | } 22 | applyHttpStub(url: string, verb: string) { 23 | return undefined; 24 | } 25 | applyServiceStub(serviceName: string, serviceVersion: string, verb: string, data: any) { 26 | return undefined; 27 | } 28 | } -------------------------------------------------------------------------------- /src/utils/actualTime.ts: -------------------------------------------------------------------------------- 1 | export default class ActualTime { 2 | 3 | // For test only 4 | static enableVirtualTimer() { 5 | ActualTime._currentTime = 1; 6 | ActualTime.getCurrentTime = () => ActualTime._currentTime; 7 | } 8 | 9 | static fastForwardActualTime(ms: number) { 10 | ActualTime._currentTime += ms; 11 | } 12 | 13 | static restore() { 14 | ActualTime.getCurrentTime = () => Date.now(); 15 | } 16 | 17 | private static _currentTime = 1; 18 | // End test only 19 | 20 | // Return current time in ms 21 | static getCurrentTime() { 22 | return Date.now(); 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/utils/conventions.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { Service } from './../globals/system'; 3 | const Utils = require('jaeger-client/dist/src/util').default; 4 | 5 | /** 6 | * Conventions values 7 | * You can override this values before instantiating application 8 | * 9 | * @export 10 | * @class Conventions 11 | */ 12 | export class Conventions { 13 | 14 | private static _instance: Conventions; 15 | 16 | static getRandomId() { 17 | return Utils.getRandom64().toString("hex"); 18 | } 19 | 20 | static clone(source, target?) { 21 | if (!source || !(typeof source === "object")) { 22 | return source; 23 | } 24 | 25 | target = target || {}; 26 | for (let key of Object.keys(source)) { 27 | let val = source[key]; 28 | if (Array.isArray(val)) { 29 | target[key] = []; 30 | val.forEach(v => target[key].push(Conventions.clone(v))); 31 | } 32 | else { 33 | target[key] = Conventions.clone(val, target[key]); 34 | } 35 | } 36 | return target; 37 | } 38 | 39 | static get instance() { 40 | if (!Conventions._instance) { 41 | Conventions._instance = new Conventions(); 42 | try { 43 | if (fs.existsSync("vulcain.conventions")) { 44 | const data = JSON.parse(fs.readFileSync("vulcain.conventions", "utf8")); 45 | Conventions.clone(data, Conventions._instance); 46 | } 47 | } 48 | catch (e) { 49 | Service.log.error(null, e, () => "Error when reading vulcain.conventions file. Custom conventions are ignored."); 50 | } 51 | } 52 | return Conventions._instance; 53 | } 54 | 55 | /** 56 | * Naming 57 | * 58 | */ 59 | defaultApplicationFolder = "api"; 60 | defaultHystrixPath = "/hystrix.stream"; 61 | defaultUrlprefix = "/api"; 62 | vulcainFileName = "vulcain.json"; 63 | defaultGraphQLSubscriptionPath = "/api/_graphql.subscriptions"; 64 | 65 | defaultStatsdDelayInMs = 10000; 66 | defaultSecretKey = "Dn~BnCG7*fjEX@Rw5uN^hWR4*AkRVKMe"; // 32 length random string 67 | defaultTokenExpiration = "20m"; // in moment format 68 | 69 | VULCAIN_SECRET_KEY = "vulcainSecretKey"; 70 | TOKEN_ISSUER = "vulcainTokenIssuer"; 71 | TOKEN_EXPIRATION = "vulcainTokenExpiration"; 72 | ENV_VULCAIN_TENANT = "VULCAIN_TENANT"; 73 | ENV_VULCAIN_DOMAIN = "VULCAIN_DOMAIN"; 74 | ENV_SERVICE_NAME = "VULCAIN_SERVICE_NAME"; 75 | ENV_SERVICE_VERSION = "VULCAIN_SERVICE_VERSION"; 76 | ENV_VULCAIN_ENV = "VULCAIN_ENV"; // 'production', 'test' or 'local' 77 | 78 | hystrix = { 79 | "hystrix.health.snapshot.validityInMilliseconds": 500, 80 | "hystrix.force.circuit.open": false, 81 | "hystrix.force.circuit.closed": false, 82 | "hystrix.circuit.enabled": true, 83 | "hystrix.circuit.sleepWindowInMilliseconds": 5000, 84 | "hystrix.circuit.errorThresholdPercentage": 50, 85 | "hystrix.circuit.volumeThreshold": 10, 86 | "hystrix.execution.timeoutInMilliseconds": 1500, 87 | "hystrix.metrics.statistical.window.timeInMilliseconds": 10000, 88 | "hystrix.metrics.statistical.window.bucketsNumber": 10, 89 | "hystrix.metrics.percentile.window.timeInMilliseconds": 10000, 90 | "hystrix.metrics.percentile.window.bucketsNumber": 10, 91 | "hystrix.isolation.semaphore.maxConcurrentRequests": 10, 92 | "hystrix.fallback.semaphore.maxConcurrentRequests": 10 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /src/utils/crypto.ts: -------------------------------------------------------------------------------- 1 | import { Service } from '../globals/system'; 2 | import * as crypto from 'crypto'; 3 | import { Conventions } from '../utils/conventions'; 4 | import { IDynamicProperty } from '../configurations/abstractions'; 5 | import { DynamicConfiguration } from '../configurations/dynamicConfiguration'; 6 | 7 | export class CryptoHelper { 8 | private static IV_LENGTH = 16; 9 | private secretKey: IDynamicProperty; 10 | 11 | constructor() { 12 | this.secretKey = DynamicConfiguration.getChainedConfigurationProperty( 13 | Conventions.instance.VULCAIN_SECRET_KEY, Conventions.instance.defaultSecretKey); 14 | } 15 | 16 | encrypt(value) { 17 | let iv = crypto.randomBytes(CryptoHelper.IV_LENGTH); 18 | let cipher = crypto.createCipheriv('aes-256-cbc', this.secretKey.value, iv); 19 | let encrypted: Buffer = cipher.update(value); 20 | encrypted = Buffer.concat([encrypted, cipher.final()]); 21 | return iv.toString('base64') + ":" + encrypted.toString('base64'); 22 | } 23 | 24 | decrypt(value: string) { 25 | let parts = value.split(':'); 26 | let iv = Buffer.from(parts.shift(), 'base64'); 27 | let encryptedText = Buffer.from(parts.join(':'), 'base64'); 28 | let decipher = crypto.createDecipheriv('aes-256-cbc', this.secretKey.value, iv); 29 | let decrypted: Buffer = decipher.update(encryptedText); 30 | decrypted = Buffer.concat([decrypted, decipher.final()]); 31 | return decrypted.toString(); 32 | } 33 | 34 | static hash(str: string) { 35 | if (!str) 36 | throw new Error("You must provide a value to hash"); 37 | return crypto.createHash('md5').update(str).digest('hex'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/utils/files.ts: -------------------------------------------------------------------------------- 1 | import * as Path from 'path'; 2 | import * as fs from 'fs'; 3 | import { Service } from './../globals/system'; 4 | import { Conventions } from './conventions'; 5 | 6 | export class Files 7 | { 8 | private static _configFilePath: string|undefined|null; 9 | static findConfigurationFile() { 10 | if (Files._configFilePath === undefined) 11 | return Files._configFilePath; 12 | 13 | let fileName = Conventions.instance.vulcainFileName; 14 | let filePath = Path.join(process.cwd(), fileName); 15 | if (fs.existsSync(filePath)) 16 | { 17 | return Files._configFilePath = filePath; 18 | } 19 | 20 | return Files._configFilePath = null; 21 | } 22 | 23 | static traverse(dir: string, callback?: (n, v) => void, filter?: (fileName) => boolean) 24 | { 25 | if(!filter) 26 | filter = (fn) => Path.extname( fn ) === ".js" && fn[0] !== "_"; 27 | 28 | if( fs.existsSync( dir ) ) 29 | { 30 | fs.readdirSync( dir ).forEach( fn => 31 | { 32 | if( filter(fn) ) 33 | { 34 | try 35 | { 36 | let c = require( Path.join( dir, fn ) ); 37 | if( !c || !callback) return; 38 | for( let ctl in c ) 39 | { 40 | callback( ctl, c[ctl] ); 41 | } 42 | } 43 | catch(err) 44 | { 45 | Service.log.error(null, err, ()=> `ERROR when trying to load component ${fn}`); 46 | process.exit(1); 47 | } 48 | } 49 | else 50 | { 51 | let fullPath = Path.join( dir, fn ); 52 | if( fs.statSync( fullPath ).isDirectory() ) 53 | { 54 | Files.traverse( Path.join( dir, fn ), callback ); 55 | } 56 | } 57 | }); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /src/utils/reflector.ts: -------------------------------------------------------------------------------- 1 | import { Preloader } from '../preloader'; 2 | import 'reflect-metadata'; 3 | 4 | const sym = Symbol.for("vulcain:metadata"); 5 | 6 | export class Reflector { 7 | 8 | static getInheritedMetadata(key, target) { 9 | let metadata; 10 | if (target) { 11 | metadata = { 12 | ...Reflector.getInheritedMetadata(key, Object.getPrototypeOf(target)), 13 | ...Reflect.getOwnMetadata(key, target) 14 | }; 15 | 16 | if(target.prototype) { 17 | metadata = { ...metadata, ...Reflect.getOwnMetadata(key, target.prototype) }; 18 | } 19 | } 20 | return metadata || {}; 21 | } 22 | 23 | static getMetadata(target, key?) { 24 | return key 25 | ? Reflect.getOwnMetadata(sym, target.prototype, key) || {} 26 | : Reflector.getInheritedMetadata(sym, target); 27 | } 28 | } 29 | 30 | export function Metadata(name: string, metadata: T) { 31 | return (target, key?) => { 32 | let metadatas = Reflect.getOwnMetadata(sym, target, key) || {}; 33 | metadatas[name] = metadata; 34 | Reflect.defineMetadata(sym, metadatas, target, key); 35 | }; 36 | } -------------------------------------------------------------------------------- /test/command/circuitBreaker.spec.ts: -------------------------------------------------------------------------------- 1 | import { CircuitBreakerFactory } from "../../src/commands/circuitBreaker"; 2 | import { CommandProperties } from "../../src/commands/commandProperties"; 3 | import { ICommandMetrics, CommandMetricsFactory } from "../../src/commands/metrics/commandMetricsFactory"; 4 | import { expect } from 'chai'; 5 | import { DynamicConfiguration } from '../../src/configurations/dynamicConfiguration'; 6 | import ActualTime from "../../src/utils/actualTime"; 7 | 8 | beforeEach(function () { 9 | ActualTime.enableVirtualTimer(); 10 | DynamicConfiguration.reset(); 11 | CommandMetricsFactory.resetCache(); 12 | CircuitBreakerFactory.resetCache(); 13 | }); 14 | 15 | function getCBOptions(commandKey) { 16 | 17 | return new CommandProperties(commandKey, commandKey, { 18 | circuitBreakerSleepWindowInMilliseconds: 1000, 19 | circuitBreakerErrorThresholdPercentage: 10, 20 | circuitBreakerRequestVolumeThreshold: 1 21 | } 22 | ); 23 | } 24 | 25 | describe("CircuitBreaker", function () { 26 | 27 | it("should cache instances in the factory", function () { 28 | let cb = CircuitBreakerFactory.getOrCreate(getCBOptions("Test")); 29 | expect(cb).to.not.be.undefined; 30 | expect(CircuitBreakerFactory.getCache().size).to.equal(1); 31 | cb = CircuitBreakerFactory.getOrCreate(getCBOptions("AnotherTest")); 32 | expect(cb).to.not.be.undefined; 33 | expect(CircuitBreakerFactory.getCache().size).to.equal(2); 34 | }); 35 | 36 | it("should open circuit if error threshold is greater than error percentage", function () { 37 | let options = getCBOptions("Test1"); 38 | let cb = CircuitBreakerFactory.getOrCreate(options); 39 | let metrics = CommandMetricsFactory.getOrCreate(options); 40 | metrics.markSuccess(); 41 | metrics.markSuccess(); 42 | metrics.markSuccess(); 43 | metrics.markSuccess(); 44 | metrics.markFailure(); 45 | metrics.markFailure(); 46 | metrics.markFailure(); 47 | metrics.markFailure(); 48 | metrics.markFailure(); 49 | metrics.markFailure(); 50 | expect(cb.isOpen()).to.be.true; 51 | }); 52 | 53 | it("should not open circuit if the volume has not reached threshold", function () { 54 | let options = getCBOptions("Test2"); 55 | options.circuitBreakerRequestVolumeThreshold.set(2); 56 | options.circuitBreakerErrorThresholdPercentage.set(50); 57 | 58 | let cb = CircuitBreakerFactory.getOrCreate(options); 59 | let metrics = CommandMetricsFactory.getOrCreate(options); 60 | metrics.markSuccess(); 61 | metrics.markFailure(); 62 | expect(cb.isOpen()).to.be.false; 63 | 64 | metrics.markFailure(); 65 | 66 | expect(cb.isOpen()).to.be.true; 67 | }); 68 | 69 | it("should retry after a configured sleep time, if the circuit was open", function () { 70 | let options = getCBOptions("Test3"); 71 | let cb = CircuitBreakerFactory.getOrCreate(options); 72 | let metrics = CommandMetricsFactory.getOrCreate(options); 73 | metrics.markSuccess(); 74 | metrics.markSuccess(); 75 | metrics.markSuccess(); 76 | metrics.markSuccess(); 77 | metrics.markFailure(); 78 | metrics.markFailure(); 79 | metrics.markFailure(); 80 | metrics.markFailure(); 81 | metrics.markFailure(); 82 | metrics.markFailure(); 83 | 84 | expect(cb.allowRequest()).to.be.false; 85 | ActualTime.fastForwardActualTime(10000); 86 | expect(cb.isOpen()).to.be.true; 87 | expect(cb.allowRequest()).to.be.true; 88 | }); 89 | 90 | it("should reset metrics after the circuit was closed again", function () { 91 | let options = getCBOptions("Test4"); 92 | let cb = CircuitBreakerFactory.getOrCreate(options); 93 | let metrics = CommandMetricsFactory.getOrCreate(options); 94 | metrics.markSuccess(); 95 | metrics.markSuccess(); 96 | metrics.markSuccess(); 97 | metrics.markSuccess(); 98 | metrics.markFailure(); 99 | metrics.markFailure(); 100 | metrics.markFailure(); 101 | metrics.markFailure(); 102 | metrics.markFailure(); 103 | metrics.markFailure(); 104 | 105 | expect(cb.allowRequest()).to.be.false; 106 | 107 | cb.markSuccess(); 108 | expect(cb.allowRequest()).to.be.true; 109 | }); 110 | 111 | }); -------------------------------------------------------------------------------- /test/command/commands.ts: -------------------------------------------------------------------------------- 1 | import { AbstractCommand } from '../../src/commands/abstractCommand'; 2 | import { Command } from '../../src/commands/commandFactory'; 3 | import { DynamicConfiguration } from '../../src/configurations/dynamicConfiguration'; 4 | import { CommandFactory } from '../../src/commands/commandFactory'; 5 | 6 | DynamicConfiguration.reset(); 7 | CommandFactory.reset(); 8 | 9 | @Command() 10 | export class TestCommand extends AbstractCommand { 11 | foo(args:string) { 12 | this.setMetricsTags("verb", {"test":"true"}); 13 | return Promise.resolve(args); 14 | } 15 | } 16 | 17 | @Command({ executionTimeoutInMilliseconds: 100 }) 18 | export class TestCommandTimeout extends AbstractCommand { 19 | runAsync(args) { 20 | this.setMetricsTags("verb", {"test":"true"}); 21 | return new Promise((resolve, reject) => { setTimeout(resolve, 300); }); 22 | } 23 | } 24 | 25 | @Command({ executionTimeoutInMilliseconds: 100 }) 26 | export class TestCommandFallback extends AbstractCommand { 27 | runAsync(args) { 28 | this.setMetricsTags("verb", {"test":"true"}); 29 | return new Promise((resolve, reject) => { 30 | throw new Error("rejected"); 31 | }); 32 | } 33 | 34 | fallbackAsync(args) { 35 | return Promise.resolve("fallback"); 36 | } 37 | } 38 | 39 | @Command({ executionTimeoutInMilliseconds: 100, circuitBreakerForceOpened: true }) 40 | export class TestCommandCircuitOpen extends AbstractCommand { 41 | runAsync(args) { 42 | this.setMetricsTags("verb", {test:"true"}); 43 | return Promise.resolve(args); 44 | } 45 | 46 | fallbackAsync(args) { 47 | return Promise.resolve("fallback"); 48 | } 49 | } -------------------------------------------------------------------------------- /test/configurations/crypto.spec.ts: -------------------------------------------------------------------------------- 1 | import { CryptoHelper } from '../../src/utils/crypto'; 2 | import { expect } from 'chai'; 3 | 4 | let plainText = "abcdefghijklmnopqrstuvwxyz\n"; 5 | let crypto = new CryptoHelper(); 6 | 7 | describe('CryptoHelper', function () { 8 | 9 | it('should decrypt an encrypted value', function () { 10 | let encrypted = crypto.encrypt(plainText); 11 | expect(crypto.decrypt(encrypted)).to.equal(plainText); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/configurations/system.spec.ts: -------------------------------------------------------------------------------- 1 | import {expect} from "chai"; 2 | import {Service} from "../../src/globals/system"; 3 | 4 | let urlExcepted = "http://localhost:8080/api/alert?tag=RED&tag=BLUE&in=00001"; 5 | let urlExceptedPath = "http://localhost:8080/api/alert/id"; 6 | 7 | describe('SystemHelper', function () { 8 | 9 | it('should create url with query string', function () { 10 | expect(Service.createUrl("http://localhost:8080/api/alert?tag=RED&tag=BLUE", { 11 | in: "00001" 12 | })).to.equal(urlExcepted); 13 | }); 14 | 15 | it('should create url with path', function () { 16 | expect(Service.createUrl("http://localhost:8080/api/alert", "id")).to.equal(urlExceptedPath); 17 | }); 18 | 19 | 20 | it('should expected an error with querystring and path', function () { 21 | expect(function () { 22 | Service.createUrl("http://localhost:8080/api/alert?type=RED", "id"); 23 | }).to.throw(Error); 24 | }); 25 | 26 | 27 | }); 28 | -------------------------------------------------------------------------------- /test/defaultHandlers/defaultHandlers.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { DefaultActionHandler, DefaultQueryHandler, ActionHandler, Model, Property, IRequestContext, IContainer } from '../../src/index'; 3 | import { DefaultServiceNames } from '../../src/di/annotations'; 4 | import { TestContext } from '../../src/pipeline/testContext'; 5 | import { Query } from '../../src/pipeline/handlers/query/annotations.query'; 6 | import { QueryHandler } from '../../src/pipeline/handlers/query/annotations.queryHandler'; 7 | 8 | @Model() 9 | class TestModel { 10 | @Property({ type: "string", required: true }) 11 | firstName: string; 12 | @Property({ type: "string", required: true, isKey: true }) 13 | lastName: string; 14 | @Property({ type: "number" }) 15 | Date: number; 16 | } 17 | 18 | @ActionHandler({ schema: "TestModel", scope: "?" }) 19 | class TestActionHandler extends DefaultActionHandler { 20 | } 21 | 22 | @QueryHandler({ scope: "?", schema: "TestModel", serviceName: "TestQueryService" }) 23 | class TestQueryHandler extends DefaultQueryHandler { 24 | } 25 | 26 | let context = new TestContext(); 27 | 28 | describe("Default action handler", function () { 29 | 30 | it("should register query handler as a service", () => { 31 | expect(context.rootContainer.get("TestQueryService")).to.be.not.null; 32 | }); 33 | 34 | it("should create an entity", async function () { 35 | const actionHandler = context.createHandler(TestActionHandler); 36 | let entity = { firstName: "elvis", lastName: "Presley" }; 37 | await actionHandler.create(entity); 38 | 39 | let query = context.getService("TestQueryService"); 40 | entity = await query.get({ lastName: "Presley" }); 41 | expect(entity).to.be.not.null; 42 | }); 43 | 44 | }); 45 | 46 | -------------------------------------------------------------------------------- /test/defaultHandlers/overrideHandlers.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { DefaultActionHandler, ActionHandler, Action, DefaultServiceNames } from '../../src/index'; 3 | import { TestContext } from '../../src/pipeline/testContext'; 4 | import { ServiceDescriptors } from '../../src/pipeline/handlers/descriptions/serviceDescriptions'; 5 | import { IdArguments } from '../../src/defaults/crudHandlers'; 6 | 7 | @ActionHandler({ scope: "?" }) 8 | class TestActionHandler extends DefaultActionHandler { 9 | @Action({name: "new", description: "rename create action"}) 10 | create(entity) { 11 | return super.create(entity); 12 | } 13 | } 14 | 15 | let context:TestContext; 16 | beforeEach(() => { 17 | context = new TestContext(); // Initialize context 18 | }); 19 | 20 | describe("Default action handler", function () { 21 | it("should override existing action", async function () { 22 | let descriptor = context.getService(DefaultServiceNames.ServiceDescriptors); 23 | // TODO expect(descriptor.getHandlerInfo(context.container, null, "new")).to.be.not.null; 24 | // expect(descriptor.getHandlerInfo(context.container, null, "create")).to.be.null; 25 | }); 26 | }); 27 | 28 | -------------------------------------------------------------------------------- /test/di/container.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { LifeTime, Inject } from '../../src/di/annotations'; 3 | import { Container } from '../../src/di/containers'; 4 | 5 | export class ClassA { 6 | } 7 | 8 | export class OverrideClassA { 9 | } 10 | 11 | export class ClassB { 12 | @Inject() 13 | classA: ClassA; 14 | @Inject('ClassZ', true) 15 | classZ: ClassA; 16 | } 17 | 18 | export class ClassC { 19 | @Inject('ClassZ') 20 | classA: ClassA; 21 | } 22 | 23 | export class ClassD extends ClassB { 24 | @Inject('ClassA') 25 | classAA: ClassA; 26 | } 27 | 28 | describe("Dependency injection", function () { 29 | 30 | it("should inject singletons", () => { 31 | let container = new Container(); 32 | container.injectSingleton(ClassA); 33 | container.injectSingleton(ClassB); 34 | 35 | let clazz = container.get('ClassB'); 36 | expect(clazz.classA).to.be.not.null; 37 | }); 38 | 39 | it("should override component", () => { 40 | let container = new Container(); 41 | container.injectInstance(ClassA, "C1"); 42 | let clazz = container.get('C1'); 43 | expect(clazz.name).to.be.equal("ClassA"); 44 | 45 | container.injectInstance(ClassB, "C1"); 46 | 47 | clazz = container.get('C1'); 48 | expect(clazz.name).to.be.equal("ClassB"); 49 | }); 50 | 51 | it("should get multiple components", () => { 52 | let container = new Container(); 53 | container.injectSingleton(ClassA, "C1"); 54 | container.injectSingleton(OverrideClassA, "C1"); 55 | let classes = container.getList('C1'); 56 | expect(classes.length).to.be.equal(2); 57 | }); 58 | 59 | it("should inject parent properties", () => { 60 | let container = new Container(); 61 | container.injectSingleton(ClassA); 62 | container.injectSingleton(ClassB); 63 | container.injectSingleton(ClassD); 64 | 65 | let clazz = container.get('ClassD'); 66 | expect(clazz.classA).to.be.not.null; 67 | expect(clazz.classAA).to.be.not.null; 68 | }); 69 | 70 | it("should failed if component does not exist", () => { 71 | let container = new Container(); 72 | container.injectSingleton(ClassA); 73 | container.injectSingleton(ClassB); 74 | container.injectSingleton(ClassC); 75 | 76 | expect(() => { 77 | let clazz = container.get('ClassC'); 78 | }) 79 | .to.throw(); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/http/hystrixSSEStream.spec.ts: -------------------------------------------------------------------------------- 1 | import { HystrixSSEStream } from "../../src/commands/http/hystrixSSEStream"; 2 | import { CommandFactory } from '../../src/commands/commandFactory'; 3 | import { CommandProperties } from "../../src/commands/commandProperties"; 4 | import { CommandMetricsFactory } from "../../src/commands/metrics/commandMetricsFactory"; 5 | import { AbstractCommand } from '../../src/commands/abstractCommand'; 6 | import { Command } from '../../src/commands/commandFactory'; 7 | import { TestContext } from '../../src/pipeline/testContext'; 8 | 9 | @Command() 10 | export class HystrixSSECommand1 extends AbstractCommand { 11 | runAsync(args) { 12 | this.setMetricsTags("verb", { test: "true" }); 13 | return new Promise((resolve, reject) => { 14 | setTimeout(function () { 15 | resolve(args); 16 | }, 200); 17 | }); 18 | } 19 | } 20 | 21 | let context = new TestContext(); 22 | 23 | describe("HystrixSSEStream", function () { 24 | 25 | function executeCommand(commandKey) { 26 | let command = CommandFactory.createDynamicCommand(context.context, commandKey); 27 | command.runAsync("success"); 28 | } 29 | 30 | it("should poll metrics every second", function (done) { 31 | executeCommand("HystrixSSECommand1"); 32 | executeCommand("HystrixSSECommand1"); 33 | 34 | setTimeout(function () { 35 | executeCommand("HystrixSSECommand1"); 36 | let stream = HystrixSSEStream.toObservable(500); 37 | let subscription = stream.subscribe( 38 | function (next) { 39 | subscription.unsubscribe(); 40 | done(); 41 | } 42 | ); 43 | }, 1001); 44 | }); 45 | }); -------------------------------------------------------------------------------- /test/metrics/counterBucket.spec.ts: -------------------------------------------------------------------------------- 1 | import Bucket from "../../src/commands/metrics/hystrix/counterBucket"; 2 | import RollingNumberEvent from "../../src/commands/metrics/hystrix/rollingNumberEvent"; 3 | import { expect } from 'chai'; 4 | 5 | describe("CounterBucket", function () { 6 | let underTest; 7 | 8 | beforeEach(function () { 9 | underTest = new Bucket(); 10 | }); 11 | 12 | it("should increment value for a valid event", function () { 13 | underTest.increment(RollingNumberEvent.SUCCESS); 14 | expect(underTest.get(RollingNumberEvent.SUCCESS)).to.equal(1); 15 | underTest.increment(RollingNumberEvent.SUCCESS); 16 | expect(underTest.get(RollingNumberEvent.SUCCESS)).to.equal(2); 17 | }); 18 | 19 | it("should return 0, if event was not recorded yet", function () { 20 | expect(underTest.get(RollingNumberEvent.FAILURE)).to.equal(0); 21 | }); 22 | 23 | it("should throw exception, if an invalid event is passed", function () { 24 | expect(function () { underTest.get("invalid"); }).to.throw(Error); 25 | expect(function () { underTest.increment("invalid"); }).to.throw(Error); 26 | }); 27 | }); -------------------------------------------------------------------------------- /test/metrics/percentileBucket.spec.ts: -------------------------------------------------------------------------------- 1 | import Bucket from "../../src/commands/metrics/hystrix/percentileBucket"; 2 | import RollingNumberEvent from "../../src/commands/metrics/hystrix/rollingNumberEvent"; 3 | import { expect } from 'chai'; 4 | 5 | describe("PercentileBucket", function () { 6 | let underTest; 7 | 8 | beforeEach(function () { 9 | underTest = new Bucket(5000); 10 | }); 11 | 12 | it("should add value to the bucket values", function () { 13 | underTest.addValue(1); 14 | expect(underTest.values).not.to.be.undefined; 15 | underTest.addValue(1); 16 | expect(underTest.values.length).to.equal(2); 17 | }); 18 | }); -------------------------------------------------------------------------------- /test/metrics/rollingNumber.spec.ts: -------------------------------------------------------------------------------- 1 | import RollingNumberEvent from "../../src/commands/metrics/hystrix/rollingNumberEvent"; 2 | import { RollingNumber } from "../../src/commands/metrics/hystrix/rollingNumber"; 3 | import { expect } from 'chai'; 4 | import ActualTime from '../../src/utils/actualTime'; 5 | 6 | describe("RollingNumber", function () { 7 | 8 | beforeEach(function () { 9 | ActualTime.enableVirtualTimer(); 10 | }); 11 | 12 | it("should be initialised with option values", function () { 13 | let underTest = new RollingNumber(5000, 5); 14 | expect((underTest).windowLength).to.equal(5000); 15 | expect((underTest).numberOfBuckets).to.equal(5); 16 | }); 17 | 18 | it("should increment a value in the latest bucket", function () { 19 | let underTest = new RollingNumber(60000, 5); 20 | let lastBucket = underTest.getCurrentBucket(); 21 | underTest.increment(RollingNumberEvent.SUCCESS); 22 | underTest.increment(RollingNumberEvent.SUCCESS); 23 | expect(lastBucket.get(RollingNumberEvent.SUCCESS)).to.equal(2); 24 | }); 25 | 26 | it("should roll the last bucket", function () { 27 | ActualTime.enableVirtualTimer(); 28 | try { 29 | let underTest = new RollingNumber(10000, 10); 30 | 31 | underTest.increment(RollingNumberEvent.SUCCESS); 32 | ActualTime.fastForwardActualTime(1001); 33 | 34 | underTest.increment(RollingNumberEvent.SUCCESS); 35 | expect(underTest.length).to.equal(2); 36 | } 37 | finally { 38 | ActualTime.restore(); 39 | } 40 | }); 41 | 42 | it("should reset the window if no activity was reported for the period longer than the window itself", function () { 43 | ActualTime.enableVirtualTimer(); 44 | try { 45 | let underTest = new RollingNumber(1000, 2); 46 | underTest.increment(RollingNumberEvent.SUCCESS); 47 | ActualTime.fastForwardActualTime(501); 48 | underTest.increment(RollingNumberEvent.SUCCESS); 49 | expect(underTest.length).to.equal(2); 50 | expect(underTest.getRollingSum(RollingNumberEvent.SUCCESS)).to.equal(2); 51 | ActualTime.fastForwardActualTime(1002); 52 | 53 | underTest.increment(RollingNumberEvent.SUCCESS); 54 | expect(underTest.getRollingSum(RollingNumberEvent.SUCCESS)).to.equal(1); 55 | } 56 | finally { 57 | ActualTime.restore(); 58 | } 59 | }); 60 | 61 | it("should return the sum of the values from all buckets", function () { 62 | ActualTime.enableVirtualTimer(); 63 | try { 64 | let underTest = new RollingNumber(10000, 10); 65 | 66 | underTest.increment(RollingNumberEvent.SUCCESS); 67 | 68 | ActualTime.fastForwardActualTime(1500); 69 | underTest.increment(RollingNumberEvent.SUCCESS); 70 | underTest.increment(RollingNumberEvent.SUCCESS); 71 | underTest.increment(RollingNumberEvent.SUCCESS); 72 | expect(underTest.length).to.equal(2); 73 | expect(underTest.getRollingSum(RollingNumberEvent.SUCCESS)).to.equal(4); 74 | } 75 | finally { 76 | ActualTime.restore(); 77 | } 78 | }); 79 | }); -------------------------------------------------------------------------------- /test/metrics/rollingPercentile.spec.ts: -------------------------------------------------------------------------------- 1 | import { RollingPercentile } from "../../src/commands/metrics/hystrix/rollingPercentile"; 2 | import { expect } from 'chai'; 3 | import ActualTime from '../../src/utils/actualTime'; 4 | 5 | function addExecutionTimes(rollingPercentile) { 6 | rollingPercentile.addValue(1); 7 | rollingPercentile.addValue(2); 8 | rollingPercentile.addValue(3); 9 | rollingPercentile.addValue(10); 10 | rollingPercentile.addValue(8); 11 | rollingPercentile.addValue(4); 12 | rollingPercentile.addValue(3); 13 | } 14 | 15 | describe("RollingPercentile", function () { 16 | it("should return 0 values before the first roll", function () { 17 | let underTest = new RollingPercentile(10000, 10); 18 | addExecutionTimes(underTest); 19 | expect(underTest.getPercentile("mean")).to.equal(0); 20 | expect(underTest.getPercentile(0)).to.equal(0); 21 | expect(underTest.getPercentile(50)).to.equal(0); 22 | 23 | } 24 | ); 25 | 26 | it("should roll the last bucket", function () { 27 | ActualTime.enableVirtualTimer(); 28 | try { 29 | let underTest = new RollingPercentile(10000, 10); 30 | underTest.addValue(1); 31 | ActualTime.fastForwardActualTime(1500); 32 | underTest.addValue(2); 33 | expect(underTest.getLength()).to.equal(2); 34 | } 35 | finally { 36 | ActualTime.restore(); 37 | } 38 | } 39 | ); 40 | 41 | it("should calculate correct percentile after the first window roll", function () { 42 | ActualTime.enableVirtualTimer(); 43 | try { 44 | let underTest = new RollingPercentile(10000, 10); 45 | addExecutionTimes(underTest); 46 | ActualTime.fastForwardActualTime(1001); 47 | expect(underTest.getPercentile("mean").toFixed(2)).to.equal("4.43"); 48 | expect(underTest.getPercentile(0).toFixed(2)).to.equal("1.00"); 49 | expect(underTest.getPercentile(50).toFixed(2)).to.equal("3.00"); 50 | } 51 | finally { 52 | ActualTime.restore(); 53 | } 54 | } 55 | ); 56 | 57 | it("should consider values values from all buckets", function () { 58 | ActualTime.enableVirtualTimer(); 59 | try { 60 | let underTest = new RollingPercentile(10000, 10); 61 | addExecutionTimes(underTest); 62 | ActualTime.fastForwardActualTime(1001); 63 | underTest.addValue(10); 64 | underTest.addValue(923); 65 | ActualTime.fastForwardActualTime(1001); 66 | expect(underTest.getPercentile("mean").toFixed(2)).to.equal("107.11"); 67 | expect(underTest.getPercentile(0).toFixed(2)).to.equal("1.00"); 68 | expect(underTest.getPercentile(50).toFixed(2)).to.equal("4.00"); 69 | } 70 | finally { 71 | ActualTime.restore(); 72 | } 73 | } 74 | ); 75 | }); -------------------------------------------------------------------------------- /test/mocks/mockService.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { StubManager } from '../../src/stubs/stubManager'; 3 | 4 | let stubDefinitions = { 5 | services: { 6 | service1: { 7 | "customer.create": 1 8 | }, 9 | service2: { 10 | "1.0": { 11 | "customer.create": 2 12 | }, 13 | "2.0": { 14 | "customer.create": 22 15 | } 16 | }, 17 | service3: 18 | { 19 | "customer.get": 20 | [ 21 | { 22 | input: {data: {id: "id3", filter: "filter1"}}, 23 | output: 333 24 | }, 25 | { 26 | input: {data: {id: "id3"}}, 27 | output: 3 28 | }, 29 | { 30 | input: {data: {id: "id33"}}, 31 | output: 33 32 | } 33 | ] 34 | } 35 | } 36 | }; 37 | 38 | let manager = new StubManager(); 39 | describe('Stub service', function () { 40 | 41 | it('should do nothing if no match', async () => { 42 | 43 | manager.initialize(stubDefinitions); 44 | 45 | expect(await manager.applyServiceStub("service1", "1.0", "Customer.delete", {})).to.be.undefined; 46 | expect(await manager.applyServiceStub("service2", "1.0", "Customer.delete", {})).to.be.undefined; 47 | expect(await manager.applyServiceStub("service2", "3.0", "Customer.get", {})).to.be.undefined; 48 | expect(await manager.applyServiceStub("service3", "1.0", "Customer.delete", {})).to.be.undefined; 49 | expect(await manager.applyServiceStub("service3", "1.0", "Customer.get", { data: {id:"id0"} })).to.be.undefined; 50 | }); 51 | 52 | it('should return value if match', async () => { 53 | 54 | manager.initialize(stubDefinitions); 55 | 56 | expect((await manager.applyServiceStub("service1", "3.0", "Customer.create", {})).content).to.be.equals(1); 57 | expect((await manager.applyServiceStub("service2", "2.0", "Customer.create", {})).content).to.be.equals(22); 58 | expect((await manager.applyServiceStub("service3", "2.0", "Customer.get", { data: {id:"id33"} })).content).to.be.equals(33); 59 | expect((await manager.applyServiceStub("service3", "2.0", "Customer.get", { data: {id:"id3", filter:"filter1"} })).content).to.be.equals(333); 60 | expect((await manager.applyServiceStub("service3", "2.0", "Customer.get", { data: {id:"id3"} })).content).to.be.equals(3); 61 | }); 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /test/providers/memory.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { MongoQueryParser } from '../../src/providers/memory/mongoQueryParser'; 3 | 4 | let entity = { name: "entity", num: 10, address: { street: "street1", city: "Paris" }, tags: ["a", "b"] }; 5 | let products = [{ _id: 1, results: [{ product: "abc", score: 10 }, { product: "xyz", score: 5 }] }, 6 | { _id: 2, results: [{ product: "abc", score: 8 }, { product: "xyz", score: 7 }] }, 7 | { _id: 3, results: [{ product: "abc", score: 7 }, { product: "xyz", score: 8 }] } 8 | ]; 9 | 10 | describe("MemoryProvider", function () { 11 | 12 | it("should filter correctly", () => { 13 | let p = new MongoQueryParser({ name: "entity" }); 14 | expect(p.execute(entity)).to.be.true; 15 | p = new MongoQueryParser({ $or: [{ num: 11 }, { name: "entity" }] }); 16 | expect(p.execute(entity)).to.be.true; 17 | p = new MongoQueryParser({ name: "entity", num: 12 }); 18 | expect(p.execute(entity)).to.be.false; 19 | p = new MongoQueryParser({ key: /shared\./, system: { "$ne": true }, deleted: { "$ne": true } }); 20 | expect(p.execute(entity)).to.be.false; 21 | }); 22 | 23 | it("should filter elemMatch", () => { 24 | let filter = { results: { $elemMatch: { product: "xyz", score: { $gte: 8 } } } }; 25 | let p = new MongoQueryParser(filter); 26 | expect(p.execute(products[0])).to.be.false; 27 | expect(p.execute(products[1])).to.be.false; 28 | expect(p.execute(products[2])).to.be.true; 29 | }); 30 | 31 | it("should filter by query operator", () => { 32 | let p = new MongoQueryParser({ results: { $size: 2 } }); 33 | expect(p.execute(products[0])).to.be.true; 34 | p = new MongoQueryParser({ tags: { $in: ["a"] } }); 35 | expect(p.execute(entity)).to.be.true; 36 | p = new MongoQueryParser({ num: { $in: [10, 20] } }); 37 | expect(p.execute(entity)).to.be.true; 38 | }); 39 | }); 40 | 41 | 42 | -------------------------------------------------------------------------------- /test/schema/bindData.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Model } from '../../src/schemas/builder/annotations.model'; 3 | import { Property } from '../../src/schemas/builder/annotations.property'; 4 | import { Domain } from '../../src/schemas/domain'; 5 | import { TestCommand } from './../command/commands'; 6 | import { TestContext } from '../../src/pipeline/testContext'; 7 | 8 | 9 | @Model() 10 | class SimpleModel { 11 | @Property({ type: "string", required: true }) 12 | text: string; 13 | @Property({ type: "uid" }) 14 | uuid: string; 15 | } 16 | 17 | @Model() 18 | class AggregateModel { 19 | @Property({ type: "SimpleModel", cardinality: "one" }) 20 | simple: SimpleModel; 21 | } 22 | 23 | @Model() 24 | class ModelWithDefaultValues { 25 | @Property({ type: 'string', required: true }) 26 | value1 = "value1"; 27 | } 28 | 29 | let context = new TestContext(); 30 | 31 | describe("Bind data", function () { 32 | 33 | it("should create uid", () => { 34 | 35 | let domain = context.rootContainer.get("Domain"); 36 | let schema = domain.getSchema("AggregateModel"); 37 | 38 | let data = { simple: { test: "test" } }; 39 | let model = schema.coerce(data); 40 | expect(model.simple.uuid).to.be.not.null; 41 | }); 42 | 43 | it("should initialize default values", () => { 44 | 45 | let domain = context.rootContainer.get("Domain"); 46 | let schema = domain.getSchema("ModelWithDefaultValues"); 47 | 48 | let data = { }; 49 | let model = schema.coerce(data); 50 | expect(model.value1).to.be.eq('value1'); 51 | }); 52 | 53 | 54 | it("should ignore not bounded property", () => { 55 | 56 | let domain = context.rootContainer.get("Domain"); 57 | let schema = domain.getSchema("ModelWithDefaultValues"); 58 | 59 | let data = { value2: "value2" }; 60 | let model = schema.coerce(data); 61 | expect(model.value2).to.be.not.null; 62 | }); 63 | }); -------------------------------------------------------------------------------- /test/schema/sensibleData.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Model } from '../../src/schemas/builder/annotations.model'; 3 | import { Property } from '../../src/schemas/builder/annotations.property'; 4 | import { Domain } from '../../src/schemas/domain'; 5 | import { TestContext } from '../../src/pipeline/testContext'; 6 | 7 | @Model() 8 | class SimpleModel { 9 | @Property({ type: "string" }) 10 | normal: string; 11 | @Property({ type: "string", sensible: true }) 12 | password: string; 13 | } 14 | 15 | @Model() 16 | class AggregateModel { 17 | @Property({ type: "SimpleModel", cardinality: "one" }) 18 | simple: SimpleModel; 19 | } 20 | 21 | let context = new TestContext(); 22 | 23 | describe("Sensible data", function () { 24 | 25 | it("should encrypt sensible properties", () => { 26 | let model = { normal: "normal", password: "password" }; 27 | let domain = context.rootContainer.get("Domain"); 28 | let schema = domain.getSchema("SimpleModel"); 29 | schema.encrypt(model); 30 | expect(model.normal).equals("normal"); 31 | expect(model.password).not.eq("password"); 32 | }); 33 | 34 | it("should encrypt embedded sensible properties", () => { 35 | let model = { simple: { normal: "normal", password: "password" } }; 36 | let domain = context.rootContainer.get("Domain"); 37 | let schema = domain.getSchema("AggregateModel"); 38 | schema.encrypt(model); 39 | expect(model.simple.normal).equals("normal"); 40 | expect(model.simple.password).not.eq("password"); 41 | }); 42 | 43 | it("should decrypt sensible properties", () => { 44 | let model = { normal: "normal", password: "password" }; 45 | let domain = context.rootContainer.get("Domain"); 46 | let schema = domain.getSchema("SimpleModel"); 47 | schema.encrypt(model); 48 | schema.decrypt(model); 49 | expect(model.normal).equals("normal"); 50 | expect(model.password).equals("password"); 51 | }); 52 | 53 | it("should remove sensible properties", () => { 54 | let model = { normal: "normal", password: "password" }; 55 | let domain = context.rootContainer.get("Domain"); 56 | let schema = domain.getSchema("SimpleModel"); 57 | schema.obfuscate(model); 58 | expect(model.normal).equals("normal"); 59 | expect(model.password).to.be.undefined; 60 | }); 61 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "declaration": true, 7 | "module": "commonjs", 8 | "target": "es2017", 9 | "watch": false, 10 | "strictFunctionTypes": true, 11 | "noUnusedParameters": false, 12 | "noUnusedLocals": false, 13 | "skipLibCheck": true, 14 | "sourceMap": true, 15 | "outDir": "lib", 16 | "rootDir": "." 17 | }, 18 | "exclude": [ 19 | "node_modules", 20 | "lib" 21 | ] 22 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-unused-expression": false, 4 | "no-duplicate-variable": true, 5 | "curly": false, 6 | "class-name": true, 7 | "semicolon":true, 8 | "triple-equals": true, 9 | "no-use-before-declare":true, 10 | "no-var-keyword": true, 11 | "prefer-for-of": true 12 | } 13 | } --------------------------------------------------------------------------------