From 36b451c32b99c2dcceb931ffc5cd4742587b387f Mon Sep 17 00:00:00 2001 From: garrettmills Date: Thu, 17 Jun 2021 19:35:31 -0500 Subject: [PATCH] Expose auth repos in context; create routes commands --- package.json | 2 + pnpm-lock.yaml | 22 ++++ src/auth/Authentication.ts | 4 + src/auth/SecurityContext.ts | 2 +- src/auth/contexts/SessionSecurityContext.ts | 2 +- src/auth/middleware/SessionAuthMiddleware.ts | 5 +- src/cli/directive/RouteDirective.ts | 71 ++++++++++++ src/cli/directive/RoutesDirective.ts | 33 ++++++ src/cli/directive/ShellDirective.ts | 1 + src/cli/service/CommandLineApplication.ts | 4 + src/http/routing/Route.ts | 28 +++++ src/orm/schema/Schema.ts | 14 +++ src/orm/schema/TableBuilder.ts | 109 +++++++++++++++++++ src/service/Routing.ts | 7 ++ 14 files changed, 300 insertions(+), 4 deletions(-) create mode 100644 src/cli/directive/RouteDirective.ts create mode 100644 src/cli/directive/RoutesDirective.ts create mode 100644 src/orm/schema/Schema.ts create mode 100644 src/orm/schema/TableBuilder.ts diff --git a/package.json b/package.json index 353d4cb..f27a782 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "dependencies": { "@types/bcrypt": "^5.0.0", "@types/busboy": "^0.2.3", + "@types/cli-table": "^0.3.0", "@types/mkdirp": "^1.0.1", "@types/negotiator": "^0.6.1", "@types/node": "^14.14.37", @@ -21,6 +22,7 @@ "@types/uuid": "^8.3.0", "bcrypt": "^5.0.1", "busboy": "^0.3.1", + "cli-table": "^0.3.6", "colors": "^1.4.0", "dotenv": "^8.2.0", "mkdirp": "^1.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 371af46..6080b65 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,6 +1,7 @@ dependencies: '@types/bcrypt': 5.0.0 '@types/busboy': 0.2.3 + '@types/cli-table': 0.3.0 '@types/mkdirp': 1.0.1 '@types/negotiator': 0.6.1 '@types/node': 14.14.37 @@ -12,6 +13,7 @@ dependencies: '@types/uuid': 8.3.0 bcrypt: 5.0.1 busboy: 0.3.1 + cli-table: 0.3.6 colors: 1.4.0 dotenv: 8.2.0 mkdirp: 1.0.4 @@ -138,6 +140,10 @@ packages: dev: false resolution: integrity: sha1-ZpetKYcyRsUw8Jo/9aQIYYJCMNU= + /@types/cli-table/0.3.0: + dev: false + resolution: + integrity: sha512-QnZUISJJXyhyD6L1e5QwXDV/A5i2W1/gl6D6YMc8u0ncPepbv/B4w3S+izVvtAg60m6h+JP09+Y/0zF2mojlFQ== /@types/glob/7.1.3: dependencies: '@types/minimatch': 3.0.4 @@ -554,6 +560,14 @@ packages: node: '>=10' resolution: integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + /cli-table/0.3.6: + dependencies: + colors: 1.0.3 + dev: false + engines: + node: '>= 0.2.0' + resolution: + integrity: sha512-ZkNZbnZjKERTY5NwC2SeMeLeifSPq/pubeRoTpdr3WchLlnZg6hEgvHkK5zL7KNFdd9PmHN8lxrENUwI3cE8vQ== /code-point-at/1.1.0: dev: false engines: @@ -582,6 +596,12 @@ packages: dev: true resolution: integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + /colors/1.0.3: + dev: false + engines: + node: '>=0.1.90' + resolution: + integrity: sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs= /colors/1.4.0: dev: false engines: @@ -2214,6 +2234,7 @@ packages: specifiers: '@types/bcrypt': ^5.0.0 '@types/busboy': ^0.2.3 + '@types/cli-table': ^0.3.0 '@types/mkdirp': ^1.0.1 '@types/negotiator': ^0.6.1 '@types/node': ^14.14.37 @@ -2227,6 +2248,7 @@ specifiers: '@typescript-eslint/parser': ^4.26.0 bcrypt: ^5.0.1 busboy: ^0.3.1 + cli-table: ^0.3.6 colors: ^1.4.0 dotenv: ^8.2.0 eslint: ^7.27.0 diff --git a/src/auth/Authentication.ts b/src/auth/Authentication.ts index bdcc555..4f76950 100644 --- a/src/auth/Authentication.ts +++ b/src/auth/Authentication.ts @@ -24,6 +24,10 @@ export class Authentication extends Unit { this.middleware.registerNamespace('@auth', this.getMiddlewareResolver()) } + /** + * Create the canonical namespace resolver for auth middleware. + * @protected + */ protected getMiddlewareResolver(): CanonicalResolver>> { return (key: string) => { return ({ diff --git a/src/auth/SecurityContext.ts b/src/auth/SecurityContext.ts index 2027ab8..a04f39f 100644 --- a/src/auth/SecurityContext.ts +++ b/src/auth/SecurityContext.ts @@ -19,7 +19,7 @@ export abstract class SecurityContext { constructor( /** The repository from which to draw users. */ - protected readonly repository: AuthenticatableRepository, + public readonly repository: AuthenticatableRepository, /** The name of this context. */ public readonly name: string, diff --git a/src/auth/contexts/SessionSecurityContext.ts b/src/auth/contexts/SessionSecurityContext.ts index 4832bd9..3d51c99 100644 --- a/src/auth/contexts/SessionSecurityContext.ts +++ b/src/auth/contexts/SessionSecurityContext.ts @@ -14,7 +14,7 @@ export class SessionSecurityContext extends SecurityContext { constructor( /** The repository from which to draw users. */ - protected readonly repository: AuthenticatableRepository, + public readonly repository: AuthenticatableRepository, ) { super(repository, 'session') } diff --git a/src/auth/middleware/SessionAuthMiddleware.ts b/src/auth/middleware/SessionAuthMiddleware.ts index 7100325..146d99e 100644 --- a/src/auth/middleware/SessionAuthMiddleware.ts +++ b/src/auth/middleware/SessionAuthMiddleware.ts @@ -6,7 +6,7 @@ import {AuthenticatableRepository} from '../types' import {SessionSecurityContext} from '../contexts/SessionSecurityContext' import {SecurityContext} from '../SecurityContext' import {ORMUserRepository} from '../orm/ORMUserRepository' -import {AuthConfig} from '../config' +import {AuthConfig, AuthenticatableRepositories} from '../config' /** * Injects a SessionSecurityContext into the request and attempts to @@ -29,6 +29,7 @@ export class SessionAuthMiddleware extends Middleware { */ protected getRepository(): AuthenticatableRepository { const config: AuthConfig | undefined = this.config.get('auth') - return this.make(config?.repositories?.session ?? ORMUserRepository) + const repo: typeof AuthenticatableRepository = AuthenticatableRepositories[config?.repositories?.session ?? 'orm'] + return this.make(repo ?? ORMUserRepository) } } diff --git a/src/cli/directive/RouteDirective.ts b/src/cli/directive/RouteDirective.ts new file mode 100644 index 0000000..d149101 --- /dev/null +++ b/src/cli/directive/RouteDirective.ts @@ -0,0 +1,71 @@ +import {Directive, OptionDefinition} from '../Directive' +import {Inject, Injectable} from '../../di' +import {Routing} from '../../service/Routing' +import Table = require('cli-table') +import {RouteHandler} from '../../http/routing/Route' + +@Injectable() +export class RouteDirective extends Directive { + @Inject() + protected readonly routing!: Routing + + getDescription(): string { + return 'Get information about a specific route' + } + + getKeywords(): string | string[] { + return ['route'] + } + + getOptions(): OptionDefinition[] { + return [ + '{route} | the path of the route', + '--method -m {value} | the HTTP method of the route', + ] + } + + async handle(): Promise { + const method: string | undefined = this.option('method') + ?.toLowerCase() + ?.trim() + + const route: string = this.option('route') + .toLowerCase() + .trim() + + this.routing.getCompiled() + .filter(match => match.getRoute().trim() === route && (!method || match.getMethod() === method)) + .tap(matches => { + if ( !matches.length ) { + this.error('No matching routes found. (Use `./ex routes` to list)') + process.exitCode = 1 + } + }) + .each(match => { + const pre = match.getMiddlewares() + .where('stage', '=', 'pre') + .map<[string, string]>(ware => [ware.stage, this.handlerToString(ware.handler)]) + + const post = match.getMiddlewares() + .where('stage', '=', 'post') + .map<[string, string]>(ware => [ware.stage, this.handlerToString(ware.handler)]) + + const maxLen = match.getMiddlewares().max(ware => this.handlerToString(ware.handler).length) + + const table = new Table({ + head: ['Stage', 'Handler'], + colWidths: [10, Math.max(maxLen, match.getDisplayableHandler().length) + 2], + }) + + table.push(...pre.toArray()) + table.push(['handler', match.getDisplayableHandler()]) + table.push(...post.toArray()) + + this.info(`\nRoute: ${match}\n\n${table}`) + }) + } + + protected handlerToString(handler: RouteHandler): string { + return typeof handler === 'string' ? handler : '(anonymous function)' + } +} diff --git a/src/cli/directive/RoutesDirective.ts b/src/cli/directive/RoutesDirective.ts new file mode 100644 index 0000000..2e0f611 --- /dev/null +++ b/src/cli/directive/RoutesDirective.ts @@ -0,0 +1,33 @@ +import {Directive} from '../Directive' +import {Inject, Injectable} from '../../di' +import {Routing} from '../../service/Routing' +import Table = require('cli-table') + +@Injectable() +export class RoutesDirective extends Directive { + @Inject() + protected readonly routing!: Routing + + getDescription(): string { + return 'List routes registered in the application' + } + + getKeywords(): string | string[] { + return ['routes'] + } + + async handle(): Promise { + const maxRouteLength = this.routing.getCompiled().max(route => String(route).length) + const maxHandlerLength = this.routing.getCompiled().max(route => route.getDisplayableHandler().length) + const rows = this.routing.getCompiled().map<[string, string]>(route => [String(route), route.getDisplayableHandler()]) + + const table = new Table({ + head: ['Route', 'Handler'], + colWidths: [maxRouteLength + 2, maxHandlerLength + 2], + }) + + table.push(...rows.toArray()) + + this.info('\n' + table) + } +} diff --git a/src/cli/directive/ShellDirective.ts b/src/cli/directive/ShellDirective.ts index 81be837..f5ab483 100644 --- a/src/cli/directive/ShellDirective.ts +++ b/src/cli/directive/ShellDirective.ts @@ -34,6 +34,7 @@ export class ShellDirective extends Directive { async handle(): Promise { const state: any = { app: this.app(), + lib: await import('../../index'), make: (target: DependencyKey, ...parameters: any[]) => this.make(target, ...parameters), } diff --git a/src/cli/service/CommandLineApplication.ts b/src/cli/service/CommandLineApplication.ts index 3dc1123..3fa9761 100644 --- a/src/cli/service/CommandLineApplication.ts +++ b/src/cli/service/CommandLineApplication.ts @@ -7,6 +7,8 @@ import {Directive} from '../Directive' import {ShellDirective} from '../directive/ShellDirective' import {TemplateDirective} from '../directive/TemplateDirective' import {RunDirective} from '../directive/RunDirective' +import {RoutesDirective} from '../directive/RoutesDirective' +import {RouteDirective} from '../directive/RouteDirective' /** * Unit that takes the place of the final unit in the application that handles @@ -42,6 +44,8 @@ export class CommandLineApplication extends Unit { this.cli.registerDirective(ShellDirective) this.cli.registerDirective(TemplateDirective) this.cli.registerDirective(RunDirective) + this.cli.registerDirective(RoutesDirective) + this.cli.registerDirective(RouteDirective) const argv = process.argv.slice(2) const match = this.cli.getDirectives() diff --git a/src/http/routing/Route.ts b/src/http/routing/Route.ts index 18119fb..38442b5 100644 --- a/src/http/routing/Route.ts +++ b/src/http/routing/Route.ts @@ -224,6 +224,34 @@ export class Route extends AppClass { super() } + /** + * Get the string-form of the route. + */ + public getRoute(): string { + return this.route + } + + /** + * Get the string-form method of the route. + */ + public getMethod(): HTTPMethod | HTTPMethod[] { + return this.method + } + + /** + * Get collection of applied middlewares. + */ + public getMiddlewares(): Collection<{ stage: 'pre' | 'post', handler: RouteHandler }> { + return this.middlewares.clone() + } + + /** + * Get the string-form of the route handler. + */ + public getDisplayableHandler(): string { + return typeof this.handler === 'string' ? this.handler : '(anonymous function)' + } + /** * Returns true if this route matches the given HTTP verb and request path. * @param method diff --git a/src/orm/schema/Schema.ts b/src/orm/schema/Schema.ts new file mode 100644 index 0000000..813efdd --- /dev/null +++ b/src/orm/schema/Schema.ts @@ -0,0 +1,14 @@ +import {Connection} from '../connection/Connection' +import {Awaitable} from '../../util' + +export abstract class Schema { + constructor( + protected readonly connection: Connection, + ) { } + + public abstract hasTable(name: string): Awaitable + + public abstract hasColumn(table: string, name: string): Awaitable + + public abstract hasColumns(table: string, name: string[]): Awaitable +} diff --git a/src/orm/schema/TableBuilder.ts b/src/orm/schema/TableBuilder.ts new file mode 100644 index 0000000..c91839b --- /dev/null +++ b/src/orm/schema/TableBuilder.ts @@ -0,0 +1,109 @@ +import {Pipe} from '../../util' + +export abstract class SchemaBuilderBase { + protected shouldDrop: 'yes'|'no'|'exists' = 'no' + + protected shouldRenameTo?: string + + constructor( + protected readonly name: string, + ) { } + + public drop(): this { + this.shouldDrop = 'yes' + return this + } + + public dropIfExists(): this { + this.shouldDrop = 'exists' + return this + } + + public rename(to: string): this { + this.shouldRenameTo = to + return this + } + + pipe(): Pipe { + return Pipe.wrap(this) + } +} + +export class ColumnBuilder extends SchemaBuilderBase { + +} + +export class IndexBuilder extends SchemaBuilderBase { + + protected fields: Set = new Set() + + protected removedFields: Set = new Set() + + protected shouldBeUnique = false + + protected shouldBePrimary = false + + protected field(name: string): this { + this.fields.add(name) + return this + } + + protected removeField(name: string): this { + this.removedFields.add(name) + this.fields.delete(name) + return this + } + + primary(): this { + this.shouldBePrimary = true + return this + } + + unique(): this { + this.shouldBeUnique = true + return this + } +} + +export class TableBuilder extends SchemaBuilderBase { + + protected columns: {[key: string]: ColumnBuilder} = {} + + protected indexes: {[key: string]: IndexBuilder} = {} + + public dropColumn(name: string): this { + this.column(name).drop() + return this + } + + public renameColumn(from: string, to: string): this { + this.column(from).rename(to) + return this + } + + public dropIndex(name: string): this { + this.index(name).drop() + return this + } + + public renameIndex(from: string, to: string): this { + this.index(from).rename(to) + return this + } + + public column(name: string) { + if ( !this.columns[name] ) { + this.columns[name] = new ColumnBuilder(name) + } + + return this.columns[name] + } + + public index(name: string) { + if ( !this.indexes[name] ) { + this.indexes[name] = new IndexBuilder(name) + } + + return this.indexes[name] + } +} diff --git a/src/service/Routing.ts b/src/service/Routing.ts index 5d0b583..d43a28c 100644 --- a/src/service/Routing.ts +++ b/src/service/Routing.ts @@ -56,4 +56,11 @@ export class Routing extends Unit { public get path(): UniversalPath { return this.app().appPath('http', 'routes') } + + /** + * Get the collection of compiled routes. + */ + public getCompiled(): Collection { + return this.compiledRoutes + } }