import * as cdk from '@aws-cdk/core'; import APIGateway from "./infrastructure/api-gateway"; import F from "./infrastructure/function"; import * as ts from "typescript"; import * as fs from "fs"; import * as path from 'path'; import {main} from "ts-node/dist/bin"; // @ts-ignore const appDir = path.dirname(require.main.filename); enum ServiceTypes { HTTP = 'http', GraphQL = 'graphql', } enum HandlerTypes { RESOLVER = 'resolver', HTTP = 'http', LAMBDA = 'lambda', EVENT = 'event', // None Api Gateway Lambda STREAM = 'stream', } export type Opts = { type: string; // Service type, like http stage: string; service: string; resource?: string; region?: string; } export type Handler = { name: string; type: HandlerTypes, assetPath: string; path?: string; // For HTTP and Lambda + Api Gateway field?: string; // For GraphQL Handlers method: string; parameter?: string; resource?: string; fields?: string[]; } export function http(path: string, methods: string[]) { return function (target: App, propertyKey: string, descriptor: PropertyDescriptor) { const method = descriptor.value; descriptor.value = function(name: string, handler: string) { const bound = method.bind(this); return bound(name, handler)({ path, type: HandlerTypes.HTTP, methods, }); } } } export function Get({ path }: { path: string }) { return function (target: App, propertyKey: string, descriptor: PropertyDescriptor) { const method = descriptor.value; descriptor.value = function(name: string, handler: string) { const bound = method.bind(this); return bound(name, handler)({ type: HandlerTypes.HTTP, path, method: 'GET', resource: target.resource, }); } } } export function Post({ path }: { path: string }) { return function (target: App, propertyKey: string, descriptor: PropertyDescriptor) { const method = descriptor.value; descriptor.value = function(name: string, handler: string) { const bound = method.bind(this); return bound(name, handler)({ type: HandlerTypes.HTTP, method: 'POST', path, resource: target.resource, }); } } } export function resolver(field: string) { return function (target: App, propertyKey: string, descriptor: PropertyDescriptor) { const method = descriptor.value; descriptor.value = function(name: string, handler: string) { const bound = method.bind(this); return bound(name, handler)({ field, type: HandlerTypes.RESOLVER, }); } } } export function subscribe(event: string) { return function (target: App, propertyKey: string, descriptor: PropertyDescriptor) { const method = descriptor.value; descriptor.value = function(name: string, handler: string) { const bound = method.bind(this); return bound(name, handler)({ event, type: HandlerTypes.EVENT, }); } } } export function lambda() { return function (target: App, propertyKey: string, descriptor: PropertyDescriptor) { const method = descriptor.value; descriptor.value = function(name: string, handler: string) { const bound = method.bind(this); return bound(name, handler)({ lambda, type: HandlerTypes.EVENT, }); } } } export function listen(stream: string) { return function (target: App, propertyKey: string, descriptor: PropertyDescriptor) { const method = descriptor.value; descriptor.value = function(name: string, handler: string) { const bound = method.bind(this); return bound(name, handler)({ stream, type: HandlerTypes.STREAM, }); } } } export function Resource(name: string) { return function (constructor: Function) { constructor.prototype.resource = name; } } type HandlerOpts = { type: HandlerTypes; parameter?: string; resource?: string; method: string; path?: string; }; export interface IServer { run(port?: string): Promise; addRoutes(routes: Handler[]): Promise; } export class App { gw?: APIGateway; public id: string; readonly app: cdk.App; protected stack: cdk.Stack; private opts: Opts; readonly handlers: Handler[]; public resource?: string; public server: IServer; constructor(server: IServer, opts: Opts) { this.handlers = []; this.app = new cdk.App(); this.opts = opts; this.id = `${this.opts.service}-${this.opts.stage}`; this.stack = new cdk.Stack(this.app, `${this.id}-stack`, { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: this.opts.region || process.env.CDK_DEFAULT_REGION, }, }); this.server = server; } public getResource() { return this.resource; } // Takes a handler, for example a lambda function, // registers it to the app, so that any infrastructure // can be generated automatically using decorators api. registerHandler(name: string, assetPath: string) { if (!this.server) { throw new Error('error 1'); } return (opts: HandlerOpts) => { this.handlers.push({ name, assetPath, parameter: opts.parameter, method: opts.method, type: opts.type, resource: opts.resource, path: opts.path, }); if (!this.opts.stage) { throw new Error('Route adder undefined'); } this.server.addRoutes([{ name, type: opts.type, method: opts.method, resource: this.opts.resource, path: opts.path, assetPath, }]).then(() => {}); } } deploy() { if (this.opts.type === ServiceTypes.HTTP) { this.gw = new APIGateway({ scope: this.stack, id: this.id, name: `${this.id}-gateway`, resource: this.resource, }); } if (!this.gw) { throw new Error('API Gateway is not set. You must set service type to `http` in order to use the http decorator'); } const scope = this.stack; const promises = this.handlers.map((handler) => { if (!handler.method) { throw new Error('You must define at least one http method'); } const dir = path.join(__dirname, '..'); const file = fs.readFileSync(`${dir}/${handler.assetPath}/index.ts`); const result = ts.transpileModule( file.toString('utf-8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, outDir: handler.assetPath, rootDir: "./", }, }, ); const outputPath = `${dir}/${handler.assetPath}out`; if (!fs.existsSync(outputPath)) { fs.mkdirSync(outputPath); } fs.writeFileSync(`${outputPath}/index.js`, result.outputText); if (this.gw) { this.gw.addMethod(handler.method, new F({ scope, id: `${handler.name}-handler-${handler.method.toLowerCase()}`, path: outputPath, }), handler.path); return true; } return false; }); Promise.all(promises) .then(() => this.app.synth()) .catch(console.error); } public run() { if (!this.server) { throw new Error('No server is defined, pass one in to your application as an opts value'); } return this.server.run(); } } export default App;