Fix packages/server
This commit is contained in:
parent
92912af839
commit
7631f085ef
@ -1,3 +1,4 @@
|
||||
packages/*/lib
|
||||
packages/*/esm
|
||||
build/
|
||||
packages/*/src/migrations
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
extends:
|
||||
- eslint:recommended
|
||||
- plugin:react/recommended
|
||||
- plugin:@typescript-eslint/eslint-recommended
|
||||
- plugin:@typescript-eslint/recommended
|
||||
- eslint:recommended
|
||||
- plugin:react/recommended
|
||||
- plugin:@typescript-eslint/eslint-recommended
|
||||
- plugin:@typescript-eslint/recommended
|
||||
settings:
|
||||
react:
|
||||
version: 'detect'
|
||||
@ -43,11 +43,13 @@ rules:
|
||||
- ignoreRestArgs: true
|
||||
overrides:
|
||||
- files:
|
||||
- '*.test.ts'
|
||||
- '*.test.tsx'
|
||||
- '*.test.ts'
|
||||
- '*.test.tsx'
|
||||
rules:
|
||||
'@typescript-eslint/no-explicit-any': off
|
||||
- files:
|
||||
- '*.js'
|
||||
- '*.js'
|
||||
rules:
|
||||
'@typescript-eslint/no-var-requires': off
|
||||
env:
|
||||
node: true
|
||||
|
||||
13
packages/common/src/auth/AuthService.ts
Normal file
13
packages/common/src/auth/AuthService.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { NewUser, UserProfile } from "../user";
|
||||
import { ChangePasswordParams } from "./ChangePasswordParams";
|
||||
import { Credentials } from "./Credentials";
|
||||
|
||||
export interface AuthService {
|
||||
createUser(credentials: NewUser): Promise<UserProfile>
|
||||
changePassword(params: ChangePasswordParams): Promise<void>
|
||||
validateCredentials(
|
||||
credentials: Credentials,
|
||||
): Promise<UserProfile | undefined>
|
||||
findOne(id: number): Promise<UserProfile | undefined>
|
||||
findUserByEmail(email: string): Promise<UserProfile | undefined>
|
||||
}
|
||||
5
packages/common/src/auth/ChangePasswordParams.ts
Normal file
5
packages/common/src/auth/ChangePasswordParams.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface ChangePasswordParams {
|
||||
userId: number
|
||||
oldPassword: string
|
||||
newPassword: string
|
||||
}
|
||||
@ -1 +1,3 @@
|
||||
export * from './AuthService'
|
||||
export * from './Credentials'
|
||||
export * from './ChangePasswordParams'
|
||||
|
||||
@ -5,6 +5,7 @@ export * from './entities'
|
||||
export * from './filterProps'
|
||||
export * from './guard'
|
||||
export * from './indexBy'
|
||||
export * from './permissions'
|
||||
export * from './StringUtils'
|
||||
export * from './team'
|
||||
export * from './types'
|
||||
|
||||
4
packages/common/src/permissions/BelongsToTeamParams.ts
Normal file
4
packages/common/src/permissions/BelongsToTeamParams.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface BelongsToTeamParams {
|
||||
userId: number
|
||||
teamId: number
|
||||
}
|
||||
6
packages/common/src/permissions/UserPermissions.ts
Normal file
6
packages/common/src/permissions/UserPermissions.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import {BelongsToTeamParams} from './BelongsToTeamParams'
|
||||
|
||||
export interface UserPermissions {
|
||||
// TODO check for role too
|
||||
belongsToTeam(params: BelongsToTeamParams): Promise<void>
|
||||
}
|
||||
2
packages/common/src/permissions/index.t
Normal file
2
packages/common/src/permissions/index.t
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './BelongsToTeamParams'
|
||||
export * from './UserPermissions'
|
||||
3
packages/common/src/permissions/index.ts
Normal file
3
packages/common/src/permissions/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './BelongsToTeamParams'
|
||||
export * from './UserPermissions'
|
||||
|
||||
4
packages/server/.eslintrc.yaml
Normal file
4
packages/server/.eslintrc.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
extends:
|
||||
- ../../.eslintrc.yaml
|
||||
rules:
|
||||
'@typescript-eslint/no-explicit-any': off
|
||||
@ -1,17 +1,17 @@
|
||||
module.exports = {
|
||||
roots: [
|
||||
'<rootDir>/src'
|
||||
'<rootDir>/src',
|
||||
],
|
||||
transform: {
|
||||
'^.+\\.tsx?$': 'ts-jest'
|
||||
'^.+\\.tsx?$': 'ts-jest',
|
||||
},
|
||||
testRegex: '(/__tests__/.*|\\.(test|spec))\\.tsx?$',
|
||||
moduleFileExtensions: [
|
||||
'ts',
|
||||
'tsx',
|
||||
'js',
|
||||
'jsx'
|
||||
'jsx',
|
||||
],
|
||||
setupFiles: ['<rootDir>/jest.setup.js'],
|
||||
verbose: false
|
||||
verbose: false,
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Server } from 'http'
|
||||
|
||||
export interface IAppServer {
|
||||
export interface AppServer {
|
||||
listen(callback?: () => void): Server
|
||||
listen(callback?: () => void): Server
|
||||
listen(portOrPath: number | string, callback?: () => void): Server
|
||||
7
packages/server/src/application/Application.ts
Normal file
7
packages/server/src/application/Application.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Database } from '../database/Database'
|
||||
import { AppServer } from './AppServer'
|
||||
|
||||
export interface Application {
|
||||
readonly server: AppServer
|
||||
readonly database: Database
|
||||
}
|
||||
@ -1,173 +1,13 @@
|
||||
import assert from 'assert'
|
||||
import { createNamespace, Namespace } from 'cls-hooked'
|
||||
import { Server } from 'http'
|
||||
import { AddressInfo } from 'net'
|
||||
import { Database } from '../database/Database'
|
||||
import { IDatabase } from '../database/IDatabase'
|
||||
import { loggerFactory, SqlLogger } from '../logger'
|
||||
import { ServerConfigurator } from './configureServer'
|
||||
import { createServer } from './createServer'
|
||||
import { IApplication } from './IApplication'
|
||||
import { IBootstrap } from './IBootstrap'
|
||||
import { IConfig } from './IConfig'
|
||||
import { IServerConfig } from './IServerConfig'
|
||||
import {AddressInfo} from 'net'
|
||||
import {Application} from './Application'
|
||||
import {Database} from '../database/Database'
|
||||
import {Config} from './Config'
|
||||
|
||||
export interface IBootstrapParams {
|
||||
readonly config: IConfig
|
||||
readonly configureServer: ServerConfigurator
|
||||
readonly namespace?: Namespace
|
||||
readonly exit?: (code: number) => void
|
||||
readonly entities?: object
|
||||
readonly migrations?: object
|
||||
}
|
||||
|
||||
// tslint:disable-next-line
|
||||
function getFunctions(obj: object): Function[] {
|
||||
return Object.keys(obj)
|
||||
.map(k => (obj as any)[k])
|
||||
.filter(f => typeof f === 'function')
|
||||
}
|
||||
|
||||
export class Bootstrap implements IBootstrap {
|
||||
protected config: IConfig
|
||||
protected configureServer: ServerConfigurator
|
||||
protected namespace: Namespace
|
||||
protected exit: (code: number) => void
|
||||
|
||||
protected server?: Server
|
||||
protected inUse: boolean = false
|
||||
readonly application: IApplication
|
||||
readonly database: IDatabase
|
||||
|
||||
constructor(params: IBootstrapParams) {
|
||||
this.config = {
|
||||
...params.config,
|
||||
app: {
|
||||
...params.config.app,
|
||||
db: {
|
||||
...params.config.app.db,
|
||||
entities: params.entities
|
||||
? getFunctions(params.entities)
|
||||
: params.config.app.db.entities,
|
||||
migrations: params.migrations
|
||||
? getFunctions(params.migrations)
|
||||
: params.config.app.db.migrations,
|
||||
},
|
||||
},
|
||||
}
|
||||
this.configureServer = params.configureServer
|
||||
this.namespace = params.namespace || createNamespace('application')
|
||||
this.exit = params.exit || process.exit
|
||||
|
||||
this.database = this.createDatabase()
|
||||
this.application = this.createApplication(this.database)
|
||||
}
|
||||
|
||||
getConfig(): IConfig {
|
||||
return this.config
|
||||
}
|
||||
|
||||
protected createDatabase(): IDatabase {
|
||||
const {namespace} = this
|
||||
const sqlLogger = new SqlLogger(loggerFactory.getLogger('sql'), namespace)
|
||||
return new Database(namespace, sqlLogger, this.getConfig().app.db)
|
||||
}
|
||||
|
||||
protected createApplication(database: IDatabase): IApplication {
|
||||
const {configureServer} = this
|
||||
return createServer(configureServer(this.getConfig(), database))
|
||||
}
|
||||
|
||||
async exec(command: string = 'listen') {
|
||||
switch (command) {
|
||||
case 'listen':
|
||||
await this.listen()
|
||||
return
|
||||
case 'migrate':
|
||||
await this.migrate()
|
||||
return
|
||||
case 'migrate-undo':
|
||||
await this.migrateUndo()
|
||||
return
|
||||
default:
|
||||
throw new Error('Unknown command: ' + command)
|
||||
}
|
||||
}
|
||||
|
||||
async listen(
|
||||
port: number | string | undefined = process.env.PORT || 3000,
|
||||
hostname: string | undefined= process.env.BIND_HOST,
|
||||
) {
|
||||
const apiLogger = loggerFactory.getLogger('api')
|
||||
try {
|
||||
await this.start(port, hostname)
|
||||
} catch (err) {
|
||||
apiLogger.error('Error starting server: %s', err.stack)
|
||||
this.exit(1)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async migrate() {
|
||||
const connection = await this.database.connect()
|
||||
await connection.runMigrations()
|
||||
await connection.close()
|
||||
}
|
||||
|
||||
async migrateUndo() {
|
||||
const connection = await this.database.connect()
|
||||
await connection.undoLastMigration()
|
||||
await connection.close()
|
||||
}
|
||||
|
||||
protected async start(
|
||||
port: number | string | undefined = process.env.PORT,
|
||||
hostname?: string,
|
||||
): Promise<void> {
|
||||
assert.ok(!this.inUse, 'Server already in use!')
|
||||
this.inUse = true
|
||||
|
||||
await this.database.connect()
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const _resolve = () => resolve()
|
||||
if (!port) {
|
||||
this.server = this.application.server.listen(_resolve)
|
||||
return
|
||||
} else if (typeof port === 'number' && hostname) {
|
||||
this.server = this.application.server.listen(port, hostname, _resolve)
|
||||
} else {
|
||||
this.server = this.application.server.listen(port, _resolve)
|
||||
}
|
||||
})
|
||||
|
||||
const apiLogger = loggerFactory.getLogger('api')
|
||||
|
||||
if (hostname) {
|
||||
apiLogger.info('Listening on %s %s', port, hostname)
|
||||
} else {
|
||||
apiLogger.info('Listening on %s', port)
|
||||
}
|
||||
}
|
||||
|
||||
getAddress(): AddressInfo | string {
|
||||
const address = this.server!.address()
|
||||
if (!address) {
|
||||
throw new Error('Server addres is null')
|
||||
}
|
||||
return address
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server!.close(err => {
|
||||
if (!err) {
|
||||
return resolve()
|
||||
}
|
||||
reject(err)
|
||||
})
|
||||
this.server = undefined
|
||||
this.inUse = false
|
||||
})
|
||||
}
|
||||
export interface Bootstrap {
|
||||
readonly application: Application
|
||||
readonly database: Database
|
||||
getConfig(): Config
|
||||
listen(port?: number | string, hostname?: string): Promise<void>
|
||||
getAddress(): AddressInfo | string
|
||||
close(): Promise<void>
|
||||
}
|
||||
|
||||
171
packages/server/src/application/CLIBootstrap.ts
Normal file
171
packages/server/src/application/CLIBootstrap.ts
Normal file
@ -0,0 +1,171 @@
|
||||
import assert from 'assert'
|
||||
import { createNamespace, Namespace } from 'cls-hooked'
|
||||
import { Server } from 'http'
|
||||
import { AddressInfo } from 'net'
|
||||
import { Database, SQLDatabase } from '../database'
|
||||
import { loggerFactory, SQLLogger } from '../logger'
|
||||
import { Application } from './Application'
|
||||
import { Bootstrap } from './Bootstrap'
|
||||
import { Config } from './Config'
|
||||
import { ServerConfigurator } from './configureServer'
|
||||
import { createServer } from './createServer'
|
||||
|
||||
export interface CLIBootstrapParams {
|
||||
readonly config: Config
|
||||
readonly configureServer: ServerConfigurator
|
||||
readonly namespace?: Namespace
|
||||
readonly exit?: (code: number) => void
|
||||
readonly entities?: object
|
||||
readonly migrations?: object
|
||||
}
|
||||
|
||||
// tslint:disable-next-line
|
||||
function getFunctions(obj: object): Function[] {
|
||||
return Object.keys(obj)
|
||||
.map(k => (obj as any)[k])
|
||||
.filter(f => typeof f === 'function')
|
||||
}
|
||||
|
||||
export class CLIBootstrap implements Bootstrap {
|
||||
protected config: Config
|
||||
protected configureServer: ServerConfigurator
|
||||
protected namespace: Namespace
|
||||
protected exit: (code: number) => void
|
||||
|
||||
protected server?: Server
|
||||
protected inUse = false
|
||||
readonly application: Application
|
||||
readonly database: Database
|
||||
|
||||
constructor(params: CLIBootstrapParams) {
|
||||
this.config = {
|
||||
...params.config,
|
||||
app: {
|
||||
...params.config.app,
|
||||
db: {
|
||||
...params.config.app.db,
|
||||
entities: params.entities
|
||||
? getFunctions(params.entities)
|
||||
: params.config.app.db.entities,
|
||||
migrations: params.migrations
|
||||
? getFunctions(params.migrations)
|
||||
: params.config.app.db.migrations,
|
||||
},
|
||||
},
|
||||
}
|
||||
this.configureServer = params.configureServer
|
||||
this.namespace = params.namespace || createNamespace('application')
|
||||
this.exit = params.exit || process.exit
|
||||
|
||||
this.database = this.createDatabase()
|
||||
this.application = this.createApplication(this.database)
|
||||
}
|
||||
|
||||
getConfig(): Config {
|
||||
return this.config
|
||||
}
|
||||
|
||||
protected createDatabase(): Database {
|
||||
const {namespace} = this
|
||||
const sqlLogger = new SQLLogger(loggerFactory.getLogger('sql'), namespace)
|
||||
return new SQLDatabase(namespace, sqlLogger, this.getConfig().app.db)
|
||||
}
|
||||
|
||||
protected createApplication(database: Database): Application {
|
||||
const {configureServer} = this
|
||||
return createServer(configureServer(this.getConfig(), database))
|
||||
}
|
||||
|
||||
async exec(command = 'listen') {
|
||||
switch (command) {
|
||||
case 'listen':
|
||||
await this.listen()
|
||||
return
|
||||
case 'migrate':
|
||||
await this.migrate()
|
||||
return
|
||||
case 'migrate-undo':
|
||||
await this.migrateUndo()
|
||||
return
|
||||
default:
|
||||
throw new Error('Unknown command: ' + command)
|
||||
}
|
||||
}
|
||||
|
||||
async listen(
|
||||
port: number | string | undefined = process.env.PORT || 3000,
|
||||
hostname: string | undefined= process.env.BIND_HOST,
|
||||
) {
|
||||
const apiLogger = loggerFactory.getLogger('api')
|
||||
try {
|
||||
await this.start(port, hostname)
|
||||
} catch (err) {
|
||||
apiLogger.error('Error starting server: %s', err.stack)
|
||||
this.exit(1)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async migrate() {
|
||||
const connection = await this.database.connect()
|
||||
await connection.runMigrations()
|
||||
await connection.close()
|
||||
}
|
||||
|
||||
async migrateUndo() {
|
||||
const connection = await this.database.connect()
|
||||
await connection.undoLastMigration()
|
||||
await connection.close()
|
||||
}
|
||||
|
||||
protected async start(
|
||||
port: number | string | undefined = process.env.PORT,
|
||||
hostname?: string,
|
||||
): Promise<void> {
|
||||
assert.ok(!this.inUse, 'Server already in use!')
|
||||
this.inUse = true
|
||||
|
||||
await this.database.connect()
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const _resolve = () => resolve()
|
||||
if (!port) {
|
||||
this.server = this.application.server.listen(_resolve)
|
||||
return
|
||||
} else if (typeof port === 'number' && hostname) {
|
||||
this.server = this.application.server.listen(port, hostname, _resolve)
|
||||
} else {
|
||||
this.server = this.application.server.listen(port, _resolve)
|
||||
}
|
||||
})
|
||||
|
||||
const apiLogger = loggerFactory.getLogger('api')
|
||||
|
||||
if (hostname) {
|
||||
apiLogger.info('Listening on %s %s', port, hostname)
|
||||
} else {
|
||||
apiLogger.info('Listening on %s', port)
|
||||
}
|
||||
}
|
||||
|
||||
getAddress(): AddressInfo | string {
|
||||
const address = this.server!.address()
|
||||
if (!address) {
|
||||
throw new Error('Server addres is null')
|
||||
}
|
||||
return address
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server!.close(err => {
|
||||
if (!err) {
|
||||
return resolve()
|
||||
}
|
||||
reject(err)
|
||||
})
|
||||
this.server = undefined
|
||||
this.inUse = false
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import {UrlWithStringQuery} from 'url'
|
||||
import {ConnectionOptions} from 'typeorm'
|
||||
|
||||
export interface IConfig {
|
||||
export interface Config {
|
||||
readonly app: {
|
||||
readonly name: string
|
||||
readonly baseUrl: UrlWithStringQuery
|
||||
@ -1,7 +0,0 @@
|
||||
import { IDatabase } from '../database/IDatabase'
|
||||
import { IAppServer } from './IAppServer'
|
||||
|
||||
export interface IApplication {
|
||||
readonly server: IAppServer
|
||||
readonly database: IDatabase
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
import {AddressInfo} from 'net'
|
||||
import {IApplication} from './IApplication'
|
||||
import {IDatabase} from '../database/IDatabase'
|
||||
import {IConfig} from './IConfig'
|
||||
|
||||
export interface IBootstrap {
|
||||
readonly application: IApplication
|
||||
readonly database: IDatabase
|
||||
getConfig(): IConfig
|
||||
listen(port?: number | string, hostname?: string): Promise<void>
|
||||
getAddress(): AddressInfo | string
|
||||
close(): Promise<void>
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
import { IConfig } from './IConfig'
|
||||
import { IDatabase } from '../database'
|
||||
import { ILogger } from '@rondo.dev/logger'
|
||||
import { IServices } from './IServices'
|
||||
import { RequestHandlerParams, ErrorRequestHandler } from 'express-serve-static-core'
|
||||
|
||||
export interface IServerMiddleware {
|
||||
path: string
|
||||
handle: RequestHandlerParams[]
|
||||
error?: ErrorRequestHandler
|
||||
}
|
||||
|
||||
export interface IServerConfig {
|
||||
readonly config: IConfig
|
||||
readonly database: IDatabase
|
||||
readonly logger: ILogger
|
||||
readonly services: IServices
|
||||
readonly globalErrorHandler: ErrorRequestHandler
|
||||
readonly framework: Record<string, IServerMiddleware>
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
import { IAuthService, IUserPermissions } from '../services'
|
||||
|
||||
export interface IServices {
|
||||
authService: IAuthService
|
||||
userPermissions: IUserPermissions
|
||||
}
|
||||
20
packages/server/src/application/ServerConfig.ts
Normal file
20
packages/server/src/application/ServerConfig.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Config } from './Config'
|
||||
import { Database } from '../database'
|
||||
import { Logger } from '@rondo.dev/logger'
|
||||
import { Services } from './Services'
|
||||
import { RequestHandlerParams, ErrorRequestHandler } from 'express-serve-static-core'
|
||||
|
||||
export interface ServerMiddleware {
|
||||
path: string
|
||||
handle: RequestHandlerParams[]
|
||||
error?: ErrorRequestHandler
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
readonly config: Config
|
||||
readonly database: Database
|
||||
readonly logger: Logger
|
||||
readonly services: Services
|
||||
readonly globalErrorHandler: ErrorRequestHandler
|
||||
readonly framework: Record<string, ServerMiddleware>
|
||||
}
|
||||
6
packages/server/src/application/Services.ts
Normal file
6
packages/server/src/application/Services.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { AuthService, UserPermissions } from '@rondo.dev/common'
|
||||
|
||||
export interface Services {
|
||||
authService: AuthService
|
||||
userPermissions: UserPermissions
|
||||
}
|
||||
@ -1,54 +1,52 @@
|
||||
import { IContext } from '@rondo.dev/common'
|
||||
import { IRoutes } from '@rondo.dev/http-types'
|
||||
import { bulkjsonrpc, jsonrpc } from '@rondo.dev/jsonrpc'
|
||||
import { json } from 'body-parser'
|
||||
import cookieParser from 'cookie-parser'
|
||||
import { IDatabase } from '../database'
|
||||
import { Database } from '../database'
|
||||
import { loggerFactory } from '../logger'
|
||||
import * as Middleware from '../middleware'
|
||||
import { TransactionalRouter } from '../router'
|
||||
import * as routes from '../routes'
|
||||
import * as rpc from '../rpc'
|
||||
import * as Services from '../services'
|
||||
import { IConfig } from './IConfig'
|
||||
import { IServerConfig } from './IServerConfig'
|
||||
import { IServices } from './IServices'
|
||||
import { SQLTeamService, SQLUserService, Context } from '../rpc'
|
||||
import { SQLAuthService, SQLUserPermissions } from '../services'
|
||||
import { Config } from './Config'
|
||||
import { ServerConfig } from './ServerConfig'
|
||||
import { Services } from './Services'
|
||||
import { Routes } from '@rondo.dev/http-types'
|
||||
import { configureAuthRoutes } from '../routes/configureAuthRoutes'
|
||||
import { TransactionMiddleware, CSRFMiddleware, RequestLogger } from '../middleware'
|
||||
|
||||
export type ServerConfigurator<
|
||||
T extends IServerConfig = IServerConfig
|
||||
T extends ServerConfig = ServerConfig
|
||||
> = (
|
||||
config: IConfig,
|
||||
database: IDatabase,
|
||||
config: Config,
|
||||
database: Database,
|
||||
) => T
|
||||
|
||||
export const configureServer: ServerConfigurator = (config, database) => {
|
||||
|
||||
const logger = loggerFactory.getLogger('api')
|
||||
|
||||
const services: IServices = {
|
||||
authService: new Services.AuthService(database),
|
||||
userPermissions: new Services.UserPermissions(database),
|
||||
const services: Services = {
|
||||
authService: new SQLAuthService(database),
|
||||
userPermissions: new SQLUserPermissions(database),
|
||||
}
|
||||
|
||||
const rpcServices = {
|
||||
userService: new rpc.UserService(database),
|
||||
teamService: new rpc.TeamService(database, services.userPermissions),
|
||||
userService: new SQLUserService(database),
|
||||
teamService: new SQLTeamService(database, services.userPermissions),
|
||||
}
|
||||
|
||||
const getContext = (req: Express.Request): IContext => ({user: req.user})
|
||||
const getContext = (req: Express.Request): Context => ({user: req.user})
|
||||
|
||||
const rpcMiddleware = jsonrpc(
|
||||
req => getContext(req),
|
||||
logger,
|
||||
// (details, invoke) => database
|
||||
// .transactionManager
|
||||
// .doInNewTransaction(() => invoke()),
|
||||
)
|
||||
|
||||
const authenticator = new Middleware.Authenticator(services.authService)
|
||||
const transactionManager = database.transactionManager
|
||||
|
||||
const createTransactionalRouter = <T extends IRoutes>() =>
|
||||
const createTransactionalRouter = <T extends Routes>() =>
|
||||
new TransactionalRouter<T>(transactionManager)
|
||||
|
||||
const globalErrorHandler = new Middleware.ErrorPageHandler(logger).handle
|
||||
@ -69,14 +67,14 @@ export const configureServer: ServerConfigurator = (config, database) => {
|
||||
sessionName: config.app.session.name,
|
||||
sessionSecret: config.app.session.secret,
|
||||
}).handle,
|
||||
new Middleware.RequestLogger(logger).handle,
|
||||
new RequestLogger(logger).handle,
|
||||
json(),
|
||||
cookieParser(config.app.session.secret),
|
||||
new Middleware.CSRFMiddleware({
|
||||
new CSRFMiddleware({
|
||||
baseUrl: config.app.baseUrl,
|
||||
cookieName: config.app.session.name + '_csrf',
|
||||
}).handle,
|
||||
new Middleware.Transaction(database.namespace).handle,
|
||||
new TransactionMiddleware(database.namespace).handle,
|
||||
authenticator.handle,
|
||||
],
|
||||
},
|
||||
@ -87,11 +85,11 @@ export const configureServer: ServerConfigurator = (config, database) => {
|
||||
api: {
|
||||
path: '/api',
|
||||
handle: [
|
||||
new routes.AuthRoutes(
|
||||
configureAuthRoutes(
|
||||
services.authService,
|
||||
authenticator,
|
||||
createTransactionalRouter(),
|
||||
).handle,
|
||||
),
|
||||
],
|
||||
error: new Middleware.ErrorApiHandler(logger).handle,
|
||||
},
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { IServerConfig } from './IServerConfig'
|
||||
import { IApplication } from './IApplication'
|
||||
import express from 'express'
|
||||
import { Application } from './Application'
|
||||
import { ServerConfig } from './ServerConfig'
|
||||
|
||||
export function createServer(appConfig: IServerConfig): IApplication {
|
||||
export function createServer(appConfig: ServerConfig): Application {
|
||||
const {config, database, framework} = appConfig
|
||||
const server = express()
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
export * from './CLIBootstrap'
|
||||
export * from './Bootstrap'
|
||||
export * from './IApplication'
|
||||
export * from './IConfig'
|
||||
export * from './IServerConfig'
|
||||
export * from './IServices'
|
||||
export * from './Application'
|
||||
export * from './Config'
|
||||
export * from './ServerConfig'
|
||||
export * from './Services'
|
||||
export * from './configureServer'
|
||||
export * from './createServer'
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import {config} from './config'
|
||||
import {Bootstrap} from './application/Bootstrap'
|
||||
import {configureServer} from './application/configureServer'
|
||||
import { CLIBootstrap } from './application'
|
||||
import { configureServer } from './application/configureServer'
|
||||
import { config } from './config'
|
||||
|
||||
export default new Bootstrap({
|
||||
export default new CLIBootstrap({
|
||||
config,
|
||||
configureServer,
|
||||
})
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import ConfigReader from '@rondo.dev/config'
|
||||
import {IConfig} from './application'
|
||||
import {Config} from './application'
|
||||
import URL from 'url'
|
||||
|
||||
const cfg = new ConfigReader(__dirname).read()
|
||||
|
||||
const baseUrl = URL.parse(cfg.get('app.baseUrl'))
|
||||
|
||||
export const config: IConfig = {
|
||||
export const config: Config = {
|
||||
app: {
|
||||
name: cfg.get('app.name'),
|
||||
assets: cfg.get('app.assets'),
|
||||
|
||||
@ -1,58 +1,15 @@
|
||||
import {IDatabase} from './IDatabase'
|
||||
import {Namespace} from 'cls-hooked'
|
||||
import {TransactionManager} from './TransactionManager'
|
||||
import {
|
||||
createConnection,
|
||||
Connection,
|
||||
ConnectionOptions,
|
||||
Logger,
|
||||
EntitySchema,
|
||||
ObjectType,
|
||||
Repository,
|
||||
} from 'typeorm'
|
||||
import { Namespace } from 'cls-hooked'
|
||||
import { Connection, EntityManager, EntitySchema, ObjectType, Repository } from 'typeorm'
|
||||
import { TransactionManager } from './TransactionManager'
|
||||
|
||||
export class Database implements IDatabase {
|
||||
protected connection?: Connection
|
||||
export interface Database {
|
||||
namespace: Namespace
|
||||
transactionManager: TransactionManager
|
||||
|
||||
constructor(
|
||||
readonly namespace: Namespace,
|
||||
protected readonly logger: Logger,
|
||||
protected readonly options: ConnectionOptions,
|
||||
) {
|
||||
this.transactionManager = new TransactionManager(
|
||||
namespace,
|
||||
this.getConnection,
|
||||
)
|
||||
}
|
||||
|
||||
async connect(): Promise<Connection> {
|
||||
this.connection = await createConnection({
|
||||
...this.options,
|
||||
logger: this.logger,
|
||||
})
|
||||
return this.connection
|
||||
}
|
||||
|
||||
getConnection = (): Connection => {
|
||||
if (!this.connection) {
|
||||
throw new Error('Not connected! Did you call connect?')
|
||||
}
|
||||
return this.connection
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.getConnection().close()
|
||||
}
|
||||
|
||||
getEntityManager() {
|
||||
return this.transactionManager.getEntityManager()
|
||||
}
|
||||
|
||||
connect(): Promise<Connection>
|
||||
getConnection(): Connection
|
||||
getEntityManager(): EntityManager
|
||||
getRepository<Entity>(
|
||||
target: ObjectType<Entity> | EntitySchema<Entity> | string,
|
||||
): Repository<Entity> {
|
||||
return this.transactionManager.getRepository(target)
|
||||
}
|
||||
|
||||
): Repository<Entity>
|
||||
close(): Promise<void>
|
||||
}
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
import {
|
||||
Connection, EntityManager, ObjectType, EntitySchema, Repository
|
||||
} from 'typeorm'
|
||||
import {ITransactionManager} from './ITransactionManager'
|
||||
import {Namespace} from 'cls-hooked'
|
||||
|
||||
export interface IDatabase {
|
||||
namespace: Namespace
|
||||
transactionManager: ITransactionManager
|
||||
connect(): Promise<Connection>
|
||||
getConnection(): Connection
|
||||
getEntityManager(): EntityManager
|
||||
getRepository<Entity>(
|
||||
target: ObjectType<Entity> | EntitySchema<Entity> | string,
|
||||
): Repository<Entity>
|
||||
close(): Promise<void>
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
import {
|
||||
EntityManager,
|
||||
EntitySchema,
|
||||
ObjectType,
|
||||
Repository,
|
||||
} from 'typeorm'
|
||||
|
||||
export const ENTITY_MANAGER = 'ENTITY_MANAGER'
|
||||
export const TRANSACTION_ID = 'TRANSACTION_ID'
|
||||
|
||||
export interface ITransactionManager {
|
||||
getEntityManager: () => EntityManager
|
||||
getRepository: <Entity>(
|
||||
target: ObjectType<Entity> | EntitySchema<Entity> | string,
|
||||
) => Repository<Entity>
|
||||
isInTransaction: () => boolean
|
||||
/**
|
||||
* Start a new or reuse an existing transaction.
|
||||
*/
|
||||
doInTransaction: <T>(
|
||||
fn: (entityManager: EntityManager) => Promise<T>) => Promise<T>
|
||||
/**
|
||||
* Always start a new transaction, regardless if there is one already in
|
||||
* progress in the current context.
|
||||
*/
|
||||
doInNewTransaction: <T>(
|
||||
fn: (entityManager: EntityManager) => Promise<T>) => Promise<T>
|
||||
}
|
||||
51
packages/server/src/database/SQLDatabase.ts
Normal file
51
packages/server/src/database/SQLDatabase.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { Namespace } from 'cls-hooked'
|
||||
import { Connection, ConnectionOptions, createConnection, EntitySchema, Logger, ObjectType, Repository } from 'typeorm'
|
||||
import { Database } from './Database'
|
||||
import { SQLTransactionManager } from './SQLTransactionManager'
|
||||
import { TransactionManager } from './TransactionManager'
|
||||
|
||||
export class SQLDatabase implements Database {
|
||||
protected connection?: Connection
|
||||
transactionManager: TransactionManager
|
||||
|
||||
constructor(
|
||||
readonly namespace: Namespace,
|
||||
protected readonly logger: Logger,
|
||||
protected readonly options: ConnectionOptions,
|
||||
) {
|
||||
this.transactionManager = new SQLTransactionManager(
|
||||
namespace,
|
||||
this.getConnection,
|
||||
)
|
||||
}
|
||||
|
||||
async connect(): Promise<Connection> {
|
||||
this.connection = await createConnection({
|
||||
...this.options,
|
||||
logger: this.logger,
|
||||
})
|
||||
return this.connection
|
||||
}
|
||||
|
||||
getConnection = (): Connection => {
|
||||
if (!this.connection) {
|
||||
throw new Error('Not connected! Did you call connect?')
|
||||
}
|
||||
return this.connection
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.getConnection().close()
|
||||
}
|
||||
|
||||
getEntityManager() {
|
||||
return this.transactionManager.getEntityManager()
|
||||
}
|
||||
|
||||
getRepository<Entity>(
|
||||
target: ObjectType<Entity> | EntitySchema<Entity> | string,
|
||||
): Repository<Entity> {
|
||||
return this.transactionManager.getRepository(target)
|
||||
}
|
||||
|
||||
}
|
||||
72
packages/server/src/database/SQLTransactionManager.ts
Normal file
72
packages/server/src/database/SQLTransactionManager.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import loggerFactory from '@rondo.dev/logger'
|
||||
import { Namespace } from 'cls-hooked'
|
||||
import shortid from 'shortid'
|
||||
import { Connection, EntityManager, EntitySchema, ObjectType, Repository } from 'typeorm'
|
||||
import { ENTITY_MANAGER, TransactionManager, TRANSACTION_ID } from './TransactionManager'
|
||||
|
||||
const log = loggerFactory.getLogger('db')
|
||||
|
||||
export type GetConnection = () => Connection
|
||||
|
||||
export class SQLTransactionManager implements TransactionManager {
|
||||
constructor(
|
||||
readonly ns: Namespace,
|
||||
readonly getConnection: GetConnection,
|
||||
) {}
|
||||
|
||||
getEntityManager = (): EntityManager => {
|
||||
const entityManager = this.ns.get(ENTITY_MANAGER) as EntityManager
|
||||
if (entityManager) {
|
||||
return entityManager
|
||||
}
|
||||
return this.getConnection().manager
|
||||
}
|
||||
|
||||
getRepository = <Entity>(
|
||||
target: ObjectType<Entity> | EntitySchema<Entity> | string,
|
||||
): Repository<Entity> => {
|
||||
return this.getEntityManager().getRepository(target)
|
||||
}
|
||||
|
||||
isInTransaction = (): boolean => {
|
||||
return !!this.ns.get(ENTITY_MANAGER)
|
||||
}
|
||||
|
||||
async doInTransaction<T>(fn: (em: EntityManager) => Promise<T>) {
|
||||
const alreadyInTransaction = this.isInTransaction()
|
||||
if (alreadyInTransaction) {
|
||||
log.info('doInTransaction: reusing existing transaction')
|
||||
return await fn(this.getEntityManager())
|
||||
}
|
||||
|
||||
log.info('doInTransaction: starting new transaction')
|
||||
return this.doInNewTransaction(fn)
|
||||
}
|
||||
|
||||
async doInNewTransaction<T>(fn: (em: EntityManager) => Promise<T>) {
|
||||
return this.ns.runAndReturn(async () => {
|
||||
this.setTransactionId(shortid())
|
||||
try {
|
||||
return await this.getConnection().manager
|
||||
.transaction(async entityManager => {
|
||||
this.setEntityManager(entityManager)
|
||||
try {
|
||||
return await fn(entityManager)
|
||||
} finally {
|
||||
this.setEntityManager(undefined)
|
||||
}
|
||||
})
|
||||
} finally {
|
||||
this.setTransactionId(undefined)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected setTransactionId(transactionId: string | undefined) {
|
||||
this.ns.set(TRANSACTION_ID, transactionId)
|
||||
}
|
||||
|
||||
protected setEntityManager(entityManager: EntityManager | undefined) {
|
||||
this.ns.set(ENTITY_MANAGER, entityManager)
|
||||
}
|
||||
}
|
||||
@ -1,72 +1,28 @@
|
||||
import loggerFactory from '@rondo.dev/logger'
|
||||
import { Namespace } from 'cls-hooked'
|
||||
import shortid from 'shortid'
|
||||
import { Connection, EntityManager, EntitySchema, ObjectType, Repository } from 'typeorm'
|
||||
import { ENTITY_MANAGER, ITransactionManager, TRANSACTION_ID } from './ITransactionManager'
|
||||
import {
|
||||
EntityManager,
|
||||
EntitySchema,
|
||||
ObjectType,
|
||||
Repository,
|
||||
} from 'typeorm'
|
||||
|
||||
const log = loggerFactory.getLogger('db')
|
||||
export const ENTITY_MANAGER = 'ENTITY_MANAGER'
|
||||
export const TRANSACTION_ID = 'TRANSACTION_ID'
|
||||
|
||||
export type TConnectionGetter = () => Connection
|
||||
|
||||
export class TransactionManager implements ITransactionManager {
|
||||
constructor(
|
||||
readonly ns: Namespace,
|
||||
readonly getConnection: TConnectionGetter,
|
||||
) {}
|
||||
|
||||
getEntityManager = (): EntityManager => {
|
||||
const entityManager = this.ns.get(ENTITY_MANAGER) as EntityManager
|
||||
if (entityManager) {
|
||||
return entityManager
|
||||
}
|
||||
return this.getConnection().manager
|
||||
}
|
||||
|
||||
getRepository = <Entity>(
|
||||
export interface TransactionManager {
|
||||
getEntityManager: () => EntityManager
|
||||
getRepository: <Entity>(
|
||||
target: ObjectType<Entity> | EntitySchema<Entity> | string,
|
||||
): Repository<Entity> => {
|
||||
return this.getEntityManager().getRepository(target)
|
||||
}
|
||||
|
||||
isInTransaction = (): boolean => {
|
||||
return !!this.ns.get(ENTITY_MANAGER)
|
||||
}
|
||||
|
||||
async doInTransaction<T>(fn: (em: EntityManager) => Promise<T>) {
|
||||
const alreadyInTransaction = this.isInTransaction()
|
||||
if (alreadyInTransaction) {
|
||||
log.info('doInTransaction: reusing existing transaction')
|
||||
return await fn(this.getEntityManager())
|
||||
}
|
||||
|
||||
log.info('doInTransaction: starting new transaction')
|
||||
return this.doInNewTransaction(fn)
|
||||
}
|
||||
|
||||
async doInNewTransaction<T>(fn: (em: EntityManager) => Promise<T>) {
|
||||
return this.ns.runAndReturn(async () => {
|
||||
this.setTransactionId(shortid())
|
||||
try {
|
||||
return await this.getConnection().manager
|
||||
.transaction(async entityManager => {
|
||||
this.setEntityManager(entityManager)
|
||||
try {
|
||||
return await fn(entityManager)
|
||||
} finally {
|
||||
this.setEntityManager(undefined)
|
||||
}
|
||||
})
|
||||
} finally {
|
||||
this.setTransactionId(undefined)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected setTransactionId(transactionId: string | undefined) {
|
||||
this.ns.set(TRANSACTION_ID, transactionId)
|
||||
}
|
||||
|
||||
protected setEntityManager(entityManager: EntityManager | undefined) {
|
||||
this.ns.set(ENTITY_MANAGER, entityManager)
|
||||
}
|
||||
) => Repository<Entity>
|
||||
isInTransaction: () => boolean
|
||||
/**
|
||||
* Start a new or reuse an existing transaction.
|
||||
*/
|
||||
doInTransaction: <T>(
|
||||
fn: (entityManager: EntityManager) => Promise<T>) => Promise<T>
|
||||
/**
|
||||
* Always start a new transaction, regardless if there is one already in
|
||||
* progress in the current context.
|
||||
*/
|
||||
doInNewTransaction: <T>(
|
||||
fn: (entityManager: EntityManager) => Promise<T>) => Promise<T>
|
||||
}
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
import {Database, ENTITY_MANAGER} from './'
|
||||
import { createNamespace } from 'cls-hooked'
|
||||
import express, { NextFunction, Request, Response } from 'express'
|
||||
import request from 'supertest'
|
||||
import express, {Request, Response, NextFunction} from 'express'
|
||||
import {createNamespace} from 'cls-hooked'
|
||||
import {CORRELATION_ID, Transaction} from '../middleware'
|
||||
import {config} from '../config'
|
||||
import {SqlLogger, loggerFactory} from '../logger'
|
||||
import { config } from '../config'
|
||||
import { loggerFactory, SQLLogger } from '../logger'
|
||||
import { CORRELATION_ID, TransactionMiddleware } from '../middleware'
|
||||
import { ENTITY_MANAGER } from './'
|
||||
import { SQLDatabase } from './SQLDatabase'
|
||||
|
||||
const ns = createNamespace('clsify-test')
|
||||
const database = new Database(
|
||||
const database = new SQLDatabase(
|
||||
ns,
|
||||
new SqlLogger(loggerFactory.getLogger('sql'), ns),
|
||||
new SQLLogger(loggerFactory.getLogger('sql'), ns),
|
||||
config.app.db,
|
||||
)
|
||||
|
||||
@ -25,7 +26,7 @@ describe('middleware/Transaction', () => {
|
||||
setImmediate(next)
|
||||
}
|
||||
|
||||
app.use(new Transaction(ns).handle)
|
||||
app.use(new TransactionMiddleware(ns).handle)
|
||||
app.use('/:id', handler)
|
||||
app.use('/:id', handler)
|
||||
app.use('/:id', handler)
|
||||
@ -61,7 +62,7 @@ describe('doInTransaction', () => {
|
||||
|
||||
let entityManager: any
|
||||
const app = express()
|
||||
app.use(new Transaction(ns).handle)
|
||||
app.use(new TransactionMiddleware(ns).handle)
|
||||
app.use('/', (req, res, next) => {
|
||||
if (entityManager) {
|
||||
ns.set(ENTITY_MANAGER, entityManager)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export * from './ITransactionManager'
|
||||
export * from './TransactionManager'
|
||||
export * from './IDatabase'
|
||||
export * from './SQLTransactionManager'
|
||||
export * from './Database'
|
||||
export * from './SQLDatabase'
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
import {
|
||||
PrimaryGeneratedColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm'
|
||||
import { CreateDateColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
|
||||
|
||||
const transformer = {
|
||||
from: (value: Date) => !isNaN(value.getTime()) ? value.toISOString() : value,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import {BaseEntity} from './BaseEntity'
|
||||
import {Column, Entity} from 'typeorm'
|
||||
import { Column, Entity } from 'typeorm'
|
||||
import { BaseEntity } from './BaseEntity'
|
||||
|
||||
@Entity()
|
||||
export class Role extends BaseEntity {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import {ISession} from '../session/ISession'
|
||||
import {Column, Entity, PrimaryColumn, Index, ManyToOne} from 'typeorm'
|
||||
import {User} from './User'
|
||||
import { Column, Entity, Index, ManyToOne, PrimaryColumn } from 'typeorm'
|
||||
import { DefaultSession } from '../session/DefaultSession'
|
||||
import { User } from './User'
|
||||
|
||||
@Entity()
|
||||
export class Session implements ISession {
|
||||
export class Session implements DefaultSession {
|
||||
@PrimaryColumn()
|
||||
id!: string
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import {BaseEntity} from './BaseEntity'
|
||||
import {Column, Entity, OneToMany, ManyToOne, Index} from 'typeorm'
|
||||
import {UserTeam} from './UserTeam'
|
||||
import {User} from './User'
|
||||
import { Column, Entity, Index, ManyToOne, OneToMany } from 'typeorm'
|
||||
import { BaseEntity } from './BaseEntity'
|
||||
import { User } from './User'
|
||||
import { UserTeam } from './UserTeam'
|
||||
|
||||
@Entity()
|
||||
export class Team extends BaseEntity {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import {BaseEntity} from './BaseEntity'
|
||||
import {Column, Entity, OneToMany} from 'typeorm'
|
||||
import {Session} from './Session'
|
||||
import {UserTeam} from './UserTeam'
|
||||
import {UserEmail} from './UserEmail'
|
||||
import { Column, Entity, OneToMany } from 'typeorm'
|
||||
import { BaseEntity } from './BaseEntity'
|
||||
import { Session } from './Session'
|
||||
import { UserEmail } from './UserEmail'
|
||||
import { UserTeam } from './UserTeam'
|
||||
|
||||
@Entity()
|
||||
export class User extends BaseEntity {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {BaseEntity} from './BaseEntity'
|
||||
import {Column, Entity, PrimaryGeneratedColumn, ManyToOne} from 'typeorm'
|
||||
import {User} from './User'
|
||||
import { Column, Entity, ManyToOne } from 'typeorm'
|
||||
import { BaseEntity } from './BaseEntity'
|
||||
import { User } from './User'
|
||||
|
||||
@Entity()
|
||||
export class UserEmail extends BaseEntity {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import {BaseEntity} from './BaseEntity'
|
||||
import {Column, Entity, ManyToOne} from 'typeorm'
|
||||
import {Role} from './Role'
|
||||
import {Team} from './Team'
|
||||
import {User} from './User'
|
||||
import { Column, Entity, ManyToOne } from 'typeorm'
|
||||
import { BaseEntity } from './BaseEntity'
|
||||
import { Role } from './Role'
|
||||
import { Team } from './Team'
|
||||
import { User } from './User'
|
||||
|
||||
@Entity()
|
||||
export class UserTeam extends BaseEntity {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import {TransformedError} from './TransformedError'
|
||||
import { TransformedError } from './TransformedError'
|
||||
|
||||
export class ErrorTransformer {
|
||||
constructor(
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import {valueOrError} from './'
|
||||
import { valueOrError } from './'
|
||||
|
||||
describe('valueOrError', () => {
|
||||
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import {ILogger} from '@rondo.dev/logger'
|
||||
import {Logger, QueryRunner} from 'typeorm'
|
||||
import {Namespace} from 'cls-hooked'
|
||||
import {CORRELATION_ID} from '../middleware/Transaction'
|
||||
import { Logger } from '@rondo.dev/logger'
|
||||
import { Namespace } from 'cls-hooked'
|
||||
import { Logger as TypeORMLogger, QueryRunner } from 'typeorm'
|
||||
import { TRANSACTION_ID } from '../database'
|
||||
import { CORRELATION_ID } from '../middleware/TransactionMiddleware'
|
||||
|
||||
export class SqlLogger implements Logger {
|
||||
export class SQLLogger implements TypeORMLogger {
|
||||
constructor(
|
||||
protected readonly logger: ILogger,
|
||||
protected readonly logger: Logger,
|
||||
protected readonly ns: Namespace,
|
||||
) {}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
export * from './SQLLogger'
|
||||
import loggerFactory from '@rondo.dev/logger'
|
||||
|
||||
export {loggerFactory}
|
||||
export { loggerFactory }
|
||||
export const getLogger = loggerFactory.getLogger
|
||||
export const apiLogger = getLogger('api')
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import express, {Application} from 'express'
|
||||
import { AuthService, Credentials } from '@rondo.dev/common'
|
||||
import { urlencoded } from 'body-parser'
|
||||
import express, { Application } from 'express'
|
||||
import request from 'supertest'
|
||||
import {Authenticator} from './Authenticator'
|
||||
import {ICredentials} from '@rondo.dev/common'
|
||||
import {IAuthService} from '../services'
|
||||
import {handlePromise} from './handlePromise'
|
||||
import {urlencoded} from 'body-parser'
|
||||
import { Authenticator } from './Authenticator'
|
||||
import { handlePromise } from './handlePromise'
|
||||
|
||||
describe('Authenticator', () => {
|
||||
|
||||
@ -18,7 +17,7 @@ describe('Authenticator', () => {
|
||||
firstName: 'test',
|
||||
lastName: 'test',
|
||||
}
|
||||
const authService = new (class implements IAuthService {
|
||||
const authService = new (class implements AuthService {
|
||||
async createUser() {
|
||||
return {id: 1, ...userInfo}
|
||||
}
|
||||
@ -26,7 +25,7 @@ describe('Authenticator', () => {
|
||||
async findOne(id: number) {
|
||||
return {id, ...userInfo}
|
||||
}
|
||||
async validateCredentials({username, password}: ICredentials) {
|
||||
async validateCredentials({username, password}: Credentials) {
|
||||
if (username === 'test' && password === 'pass') {
|
||||
return {id: 1, ...userInfo}
|
||||
return
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import {Authenticator as A, Passport} from 'passport'
|
||||
import {IAuthService} from '../services'
|
||||
import {Strategy as LocalStrategy} from 'passport-local'
|
||||
import {THandler} from './THandler'
|
||||
import {IMiddleware} from './IMiddleware'
|
||||
import { Authenticator as A, Passport } from 'passport'
|
||||
import { Strategy as LocalStrategy } from 'passport-local'
|
||||
import { Middleware } from './Middleware'
|
||||
import { Handler } from './Handler'
|
||||
import { AuthService } from '@rondo.dev/common'
|
||||
|
||||
export class Authenticator implements IMiddleware {
|
||||
export class Authenticator implements Middleware {
|
||||
|
||||
protected readonly passport: A
|
||||
readonly handle: THandler[]
|
||||
readonly handle: Handler[]
|
||||
|
||||
constructor(protected readonly authService: IAuthService) {
|
||||
constructor(protected readonly authService: AuthService) {
|
||||
this.passport = new Passport() as any
|
||||
|
||||
this.configurePassport()
|
||||
@ -22,7 +22,7 @@ export class Authenticator implements IMiddleware {
|
||||
]
|
||||
}
|
||||
|
||||
withLogInPromise: THandler = (req, res, next) => {
|
||||
withLogInPromise: Handler = (req, res, next) => {
|
||||
req.logInPromise = (user) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
req.logIn(user, err => {
|
||||
@ -72,7 +72,7 @@ export class Authenticator implements IMiddleware {
|
||||
.catch(done)
|
||||
}
|
||||
|
||||
authenticate(strategy: string | string[]): THandler {
|
||||
authenticate(strategy: string | string[]): Handler {
|
||||
return (req, res, next) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.passport.authenticate(strategy, (err: Error, user, info) => {
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import Csurf from 'csurf'
|
||||
import {THandler} from './THandler'
|
||||
import {IMiddleware} from './IMiddleware'
|
||||
import {UrlWithStringQuery} from 'url'
|
||||
import { UrlWithStringQuery } from 'url'
|
||||
import { Middleware } from './Middleware'
|
||||
import { Handler } from './Handler'
|
||||
|
||||
export interface ICSRFParams {
|
||||
export interface CSRFMiddlewareParams {
|
||||
baseUrl: UrlWithStringQuery
|
||||
cookieName: string
|
||||
}
|
||||
|
||||
export class CSRFMiddleware implements IMiddleware {
|
||||
readonly handle: THandler
|
||||
export class CSRFMiddleware implements Middleware {
|
||||
readonly handle: Handler
|
||||
|
||||
constructor(readonly params: ICSRFParams) {
|
||||
constructor(readonly params: CSRFMiddlewareParams) {
|
||||
this.handle = Csurf({
|
||||
cookie: {
|
||||
signed: true,
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import {TErrorHandler} from './TErrorHandler'
|
||||
import {ILogger} from '@rondo.dev/logger'
|
||||
import {IMiddleware} from './IMiddleware'
|
||||
import {ValidationError} from '@rondo.dev/validator'
|
||||
import { Logger } from '@rondo.dev/logger'
|
||||
import { ValidationError } from '@rondo.dev/validator'
|
||||
import { ErrorHandler } from './ErrorHandler'
|
||||
import { Middleware } from './Middleware'
|
||||
|
||||
export class ErrorApiHandler implements IMiddleware {
|
||||
constructor(readonly logger: ILogger) {}
|
||||
export class ErrorApiHandler implements Middleware {
|
||||
constructor(readonly logger: Logger) {}
|
||||
|
||||
handle: TErrorHandler = (err, req, res, next) => {
|
||||
handle: ErrorHandler = (err, req, res, next) => {
|
||||
this.logger.error('%s An API error occurred: %s',
|
||||
req.correlationId, err.stack)
|
||||
const statusCode = this.getStatus(err)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import {Request, Response, NextFunction} from 'express'
|
||||
|
||||
export type TErrorHandler =
|
||||
export type ErrorHandler =
|
||||
(err: Error, req: Request, res: Response, next: NextFunction) => any
|
||||
@ -1,11 +1,11 @@
|
||||
import {ILogger} from '@rondo.dev/logger'
|
||||
import {IMiddleware} from './IMiddleware'
|
||||
import {TErrorHandler} from './TErrorHandler'
|
||||
import { Logger } from '@rondo.dev/logger'
|
||||
import { ErrorHandler } from './ErrorHandler'
|
||||
import { Middleware } from './Middleware'
|
||||
|
||||
export class ErrorPageHandler implements IMiddleware {
|
||||
constructor(readonly logger: ILogger) {}
|
||||
export class ErrorPageHandler implements Middleware {
|
||||
constructor(readonly logger: Logger) {}
|
||||
|
||||
handle: TErrorHandler = (err, req, res, next) => {
|
||||
handle: ErrorHandler = (err, req, res, next) => {
|
||||
this.logger.error(
|
||||
'%s An error occurred: %s',
|
||||
req.correlationId, err.stack)
|
||||
|
||||
3
packages/server/src/middleware/Handler.ts
Normal file
3
packages/server/src/middleware/Handler.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
|
||||
export type Handler = (req: Request, res: Response, next: NextFunction) => any
|
||||
@ -1,6 +0,0 @@
|
||||
import {THandler} from './THandler'
|
||||
import {TErrorHandler} from './TErrorHandler'
|
||||
|
||||
export interface IMiddleware {
|
||||
handle: THandler | THandler[] | TErrorHandler
|
||||
}
|
||||
6
packages/server/src/middleware/Middleware.ts
Normal file
6
packages/server/src/middleware/Middleware.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import {Handler} from './Handler'
|
||||
import {ErrorHandler} from './ErrorHandler'
|
||||
|
||||
export interface Middleware {
|
||||
handle: Handler | Handler[] | ErrorHandler
|
||||
}
|
||||
4
packages/server/src/middleware/PromiseHandler.ts
Normal file
4
packages/server/src/middleware/PromiseHandler.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
|
||||
export type PromiseHandler<T> =
|
||||
(req: Request, res: Response, next: NextFunction) => Promise<T>
|
||||
@ -1,12 +1,12 @@
|
||||
import {THandler} from './THandler'
|
||||
import {ILogger} from '@rondo.dev/logger'
|
||||
import {IMiddleware} from './IMiddleware'
|
||||
import { Logger } from '@rondo.dev/logger'
|
||||
import shortid from 'shortid'
|
||||
import { Handler } from './Handler'
|
||||
import { Middleware } from './Middleware'
|
||||
|
||||
export class RequestLogger implements IMiddleware {
|
||||
constructor(protected readonly logger: ILogger) {}
|
||||
export class RequestLogger implements Middleware {
|
||||
constructor(protected readonly logger: Logger) {}
|
||||
|
||||
handle: THandler = (req, res, next) => {
|
||||
handle: Handler = (req, res, next) => {
|
||||
const start = Date.now()
|
||||
req.correlationId = shortid.generate()
|
||||
res.on('finish', () => {
|
||||
|
||||
@ -1,24 +1,24 @@
|
||||
import ExpressSession from 'express-session'
|
||||
import {THandler} from './THandler'
|
||||
import {IMiddleware} from './IMiddleware'
|
||||
import {ISession} from '../session/ISession'
|
||||
import {ITransactionManager} from '../database/ITransactionManager'
|
||||
import {Session as SessionEntity} from '../entities/Session'
|
||||
import {SessionStore} from '../session/SessionStore'
|
||||
import {UrlWithStringQuery} from 'url'
|
||||
import {apiLogger} from '../logger'
|
||||
import { UrlWithStringQuery } from 'url'
|
||||
import { TransactionManager } from '../database'
|
||||
import { Session as SessionEntity } from '../entities/Session'
|
||||
import { apiLogger } from '../logger'
|
||||
import { SessionStore } from '../session/SessionStore'
|
||||
import { Handler } from './Handler'
|
||||
import { Middleware } from './Middleware'
|
||||
import { DefaultSession } from '../session'
|
||||
|
||||
export interface ISessionOptions {
|
||||
transactionManager: ITransactionManager,
|
||||
baseUrl: UrlWithStringQuery,
|
||||
sessionName: string,
|
||||
sessionSecret: string | string[],
|
||||
export interface SessionMiddlewareParams {
|
||||
transactionManager: TransactionManager
|
||||
baseUrl: UrlWithStringQuery
|
||||
sessionName: string
|
||||
sessionSecret: string | string[]
|
||||
}
|
||||
|
||||
export class SessionMiddleware implements IMiddleware {
|
||||
readonly handle: THandler
|
||||
export class SessionMiddleware implements Middleware {
|
||||
readonly handle: Handler
|
||||
|
||||
constructor(readonly params: ISessionOptions) {
|
||||
constructor(readonly params: SessionMiddlewareParams) {
|
||||
this.handle = ExpressSession({
|
||||
saveUninitialized: false,
|
||||
secret: params.sessionSecret,
|
||||
@ -42,8 +42,10 @@ export class SessionMiddleware implements IMiddleware {
|
||||
})
|
||||
}
|
||||
|
||||
protected buildSession = (sessionData: Express.SessionData, sess: ISession)
|
||||
: SessionEntity => {
|
||||
protected buildSession = (
|
||||
sessionData: Express.SessionData,
|
||||
sess: DefaultSession,
|
||||
): SessionEntity => {
|
||||
return {...sess, userId: sessionData.userId }
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
import {Request, Response, NextFunction} from 'express'
|
||||
|
||||
export type THandler = (req: Request, res: Response, next: NextFunction) => any
|
||||
@ -1,4 +0,0 @@
|
||||
import {Request, Response, NextFunction} from 'express'
|
||||
|
||||
export type TPromiseHandler<T> =
|
||||
(req: Request, res: Response, next: NextFunction) => Promise<T>
|
||||
@ -1,14 +1,14 @@
|
||||
import shortid from 'shortid'
|
||||
import {IMiddleware} from './IMiddleware'
|
||||
import {THandler} from './THandler'
|
||||
import {Middleware} from './Middleware'
|
||||
import {Handler} from './Handler'
|
||||
import {Namespace} from 'cls-hooked'
|
||||
|
||||
export const CORRELATION_ID = 'CORRELATION_ID'
|
||||
|
||||
export class Transaction implements IMiddleware {
|
||||
export class TransactionMiddleware implements Middleware {
|
||||
constructor(readonly ns: Namespace) {}
|
||||
|
||||
handle: THandler = (req, res, next) => {
|
||||
handle: Handler = (req, res, next) => {
|
||||
const {ns} = this
|
||||
ns.bindEmitter(req)
|
||||
ns.bindEmitter(res)
|
||||
@ -1,8 +1,6 @@
|
||||
import {ensureLoggedInApi, ensureLoggedInSite} from './ensureLoggedIn'
|
||||
import express, {
|
||||
Application, Request, Response, NextFunction as Next,
|
||||
} from 'express'
|
||||
import express, { Application, NextFunction as Next, Request, Response } from 'express'
|
||||
import request from 'supertest'
|
||||
import { ensureLoggedInApi, ensureLoggedInSite } from './ensureLoggedIn'
|
||||
|
||||
function createMockUserMiddleware() {
|
||||
const context: {user?: any} = {}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { Request } from 'express'
|
||||
import createError from 'http-errors'
|
||||
import {Request} from 'express'
|
||||
import {THandler} from './THandler'
|
||||
import { Handler } from './Handler'
|
||||
|
||||
const isLoggedIn = (req: Request) => !!(req as any).user
|
||||
|
||||
export const ensureLoggedInApi: THandler = (req, res, next) => {
|
||||
export const ensureLoggedInApi: Handler = (req, res, next) => {
|
||||
if (!isLoggedIn(req)) {
|
||||
next(createError(401))
|
||||
return
|
||||
@ -12,7 +12,7 @@ export const ensureLoggedInApi: THandler = (req, res, next) => {
|
||||
next()
|
||||
}
|
||||
|
||||
export const ensureLoggedInSite = (redirectTo: string): THandler => {
|
||||
export const ensureLoggedInSite = (redirectTo: string): Handler => {
|
||||
return function _ensureLoggedInSite(req, res, next) {
|
||||
if (!isLoggedIn(req)) {
|
||||
res.redirect(redirectTo)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import {THandler} from './THandler'
|
||||
import {TPromiseHandler} from './TPromiseHandler'
|
||||
import { Handler } from './Handler'
|
||||
import { PromiseHandler } from './PromiseHandler'
|
||||
|
||||
export function handlePromise<T>(endpoint: TPromiseHandler<T>): THandler {
|
||||
export function handlePromise<T>(endpoint: PromiseHandler<T>): Handler {
|
||||
return (req, res, next) => {
|
||||
const promise = endpoint(req, res, next)
|
||||
promise
|
||||
|
||||
@ -2,12 +2,12 @@ export * from './Authenticator'
|
||||
export * from './CSRFMiddleware'
|
||||
export * from './ErrorApiHandler'
|
||||
export * from './ErrorPageHandler'
|
||||
export * from './IMiddleware'
|
||||
export * from './Middleware'
|
||||
export * from './RequestLogger'
|
||||
export * from './SessionMiddleware'
|
||||
export * from './TErrorHandler'
|
||||
export * from './THandler'
|
||||
export * from './TPromiseHandler'
|
||||
export * from './Transaction'
|
||||
export * from './ErrorHandler'
|
||||
export * from './Handler'
|
||||
export * from './PromiseHandler'
|
||||
export * from './TransactionMiddleware'
|
||||
export * from './ensureLoggedIn'
|
||||
export * from './handlePromise'
|
||||
|
||||
@ -4,32 +4,32 @@ import {AsyncRouter} from './AsyncRouter'
|
||||
|
||||
describe('AsyncRouter', () => {
|
||||
|
||||
interface IResponse {
|
||||
interface Response {
|
||||
value: string
|
||||
}
|
||||
|
||||
interface IParam {
|
||||
interface Param {
|
||||
param: string
|
||||
}
|
||||
|
||||
interface IHandler {
|
||||
params: IParam,
|
||||
response: IResponse,
|
||||
interface Handler {
|
||||
params: Param
|
||||
response: Response
|
||||
}
|
||||
|
||||
interface IMyApi {
|
||||
interface MyApi {
|
||||
'/test/:param': {
|
||||
get: IHandler
|
||||
post: IHandler
|
||||
put: IHandler
|
||||
delete: IHandler
|
||||
options: IHandler
|
||||
patch: IHandler
|
||||
head: {},
|
||||
get: Handler
|
||||
post: Handler
|
||||
put: Handler
|
||||
delete: Handler
|
||||
options: Handler
|
||||
patch: Handler
|
||||
head: {}
|
||||
}
|
||||
'/middleware': {
|
||||
get: {
|
||||
response: IResponse
|
||||
response: Response
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -37,7 +37,7 @@ describe('AsyncRouter', () => {
|
||||
const app = express()
|
||||
const router = express.Router()
|
||||
app.use(router)
|
||||
const asyncRouter = new AsyncRouter<IMyApi>(router)
|
||||
const asyncRouter = new AsyncRouter<MyApi>(router)
|
||||
|
||||
asyncRouter.get('/test/:param', async req => {
|
||||
return {value: req.params.param}
|
||||
@ -72,7 +72,7 @@ describe('AsyncRouter', () => {
|
||||
})
|
||||
|
||||
it('creates its own router when not provided', () => {
|
||||
const r = new AsyncRouter<IMyApi>()
|
||||
const r = new AsyncRouter<MyApi>()
|
||||
expect(r.router).toBeTruthy()
|
||||
})
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { Method, Routes } from '@rondo.dev/http-types'
|
||||
import express from 'express'
|
||||
import {IRoutes, TMethod} from '@rondo.dev/http-types'
|
||||
import {TTypedHandler, TTypedMiddleware} from './TTypedHandler'
|
||||
import { TypedHandler, TypedMiddleware } from './TypedHandler'
|
||||
|
||||
export class AsyncRouter<R extends IRoutes> {
|
||||
export class AsyncRouter<R extends Routes> {
|
||||
readonly router: express.Router
|
||||
readonly use: express.IRouterHandler<void> & express.IRouterMatcher<void>
|
||||
|
||||
@ -11,12 +11,12 @@ export class AsyncRouter<R extends IRoutes> {
|
||||
this.use = this.router.use.bind(this.router) as any
|
||||
}
|
||||
|
||||
protected addRoute<M extends TMethod, P extends keyof R & string>(
|
||||
protected addRoute<M extends Method, P extends keyof R & string>(
|
||||
method: M,
|
||||
path: P,
|
||||
...handlers: [TTypedHandler<R, P, M>] | [
|
||||
Array<TTypedMiddleware<R, P, M>>,
|
||||
TTypedHandler<R, P, M>,
|
||||
...handlers: [TypedHandler<R, P, M>] | [
|
||||
Array<TypedMiddleware<R, P, M>>,
|
||||
TypedHandler<R, P, M>,
|
||||
]
|
||||
) {
|
||||
const addRoute = this.router[method].bind(this.router as any)
|
||||
@ -31,8 +31,8 @@ export class AsyncRouter<R extends IRoutes> {
|
||||
|
||||
}
|
||||
|
||||
protected wrapHandler<M extends TMethod, P extends keyof R & string>(
|
||||
handler: TTypedHandler<R, P, M>,
|
||||
protected wrapHandler<M extends Method, P extends keyof R & string>(
|
||||
handler: TypedHandler<R, P, M>,
|
||||
): express.RequestHandler {
|
||||
return (req, res, next) => {
|
||||
handler(req, res, next)
|
||||
@ -45,9 +45,9 @@ export class AsyncRouter<R extends IRoutes> {
|
||||
|
||||
get<P extends keyof R & string>(
|
||||
path: P,
|
||||
...handlers: [TTypedHandler<R, P, 'get'>] | [
|
||||
Array<TTypedMiddleware<R, P, 'get'>>,
|
||||
TTypedHandler<R, P, 'get'>,
|
||||
...handlers: [TypedHandler<R, P, 'get'>] | [
|
||||
Array<TypedMiddleware<R, P, 'get'>>,
|
||||
TypedHandler<R, P, 'get'>,
|
||||
]
|
||||
): void {
|
||||
this.addRoute('get', path, ...handlers)
|
||||
@ -55,9 +55,9 @@ export class AsyncRouter<R extends IRoutes> {
|
||||
|
||||
post<P extends keyof R & string>(
|
||||
path: P,
|
||||
...handlers: [TTypedHandler<R, P, 'post'>] | [
|
||||
Array<TTypedMiddleware<R, P, 'post'>>,
|
||||
TTypedHandler<R, P, 'post'>,
|
||||
...handlers: [TypedHandler<R, P, 'post'>] | [
|
||||
Array<TypedMiddleware<R, P, 'post'>>,
|
||||
TypedHandler<R, P, 'post'>,
|
||||
]
|
||||
) {
|
||||
this.addRoute('post', path, ...handlers)
|
||||
@ -65,9 +65,9 @@ export class AsyncRouter<R extends IRoutes> {
|
||||
|
||||
put<P extends keyof R & string>(
|
||||
path: P,
|
||||
...handlers: [TTypedHandler<R, P, 'put'>] | [
|
||||
Array<TTypedMiddleware<R, P, 'put'>>,
|
||||
TTypedHandler<R, P, 'put'>,
|
||||
...handlers: [TypedHandler<R, P, 'put'>] | [
|
||||
Array<TypedMiddleware<R, P, 'put'>>,
|
||||
TypedHandler<R, P, 'put'>,
|
||||
]
|
||||
) {
|
||||
this.addRoute('put', path, ...handlers)
|
||||
@ -75,9 +75,9 @@ export class AsyncRouter<R extends IRoutes> {
|
||||
|
||||
delete<P extends keyof R & string>(
|
||||
path: P,
|
||||
...handlers: [TTypedHandler<R, P, 'delete'>] | [
|
||||
Array<TTypedMiddleware<R, P, 'delete'>>,
|
||||
TTypedHandler<R, P, 'delete'>,
|
||||
...handlers: [TypedHandler<R, P, 'delete'>] | [
|
||||
Array<TypedMiddleware<R, P, 'delete'>>,
|
||||
TypedHandler<R, P, 'delete'>,
|
||||
]
|
||||
) {
|
||||
this.addRoute('delete', path, ...handlers)
|
||||
@ -85,9 +85,9 @@ export class AsyncRouter<R extends IRoutes> {
|
||||
|
||||
head<P extends keyof R & string>(
|
||||
path: P,
|
||||
...handlers: [TTypedHandler<R, P, 'head'>] | [
|
||||
Array<TTypedMiddleware<R, P, 'head'>>,
|
||||
TTypedHandler<R, P, 'head'>,
|
||||
...handlers: [TypedHandler<R, P, 'head'>] | [
|
||||
Array<TypedMiddleware<R, P, 'head'>>,
|
||||
TypedHandler<R, P, 'head'>,
|
||||
]
|
||||
) {
|
||||
this.addRoute('head', path, ...handlers)
|
||||
@ -95,9 +95,9 @@ export class AsyncRouter<R extends IRoutes> {
|
||||
|
||||
options<P extends keyof R & string>(
|
||||
path: P,
|
||||
...handlers: [TTypedHandler<R, P, 'options'>] | [
|
||||
Array<TTypedMiddleware<R, P, 'options'>>,
|
||||
TTypedHandler<R, P, 'options'>,
|
||||
...handlers: [TypedHandler<R, P, 'options'>] | [
|
||||
Array<TypedMiddleware<R, P, 'options'>>,
|
||||
TypedHandler<R, P, 'options'>,
|
||||
]
|
||||
) {
|
||||
this.addRoute('options', path, ...handlers)
|
||||
@ -105,9 +105,9 @@ export class AsyncRouter<R extends IRoutes> {
|
||||
|
||||
patch<P extends keyof R & string>(
|
||||
path: P,
|
||||
...handlers: [TTypedHandler<R, P, 'patch'>] | [
|
||||
Array<TTypedMiddleware<R, P, 'patch'>>,
|
||||
TTypedHandler<R, P, 'patch'>,
|
||||
...handlers: [TypedHandler<R, P, 'patch'>] | [
|
||||
Array<TypedMiddleware<R, P, 'patch'>>,
|
||||
TypedHandler<R, P, 'patch'>,
|
||||
]
|
||||
) {
|
||||
this.addRoute('patch', path, ...handlers)
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
import express from 'express'
|
||||
import {IRoute} from '@rondo.dev/http-types'
|
||||
|
||||
export interface ITypedRequest<T extends IRoute> extends express.Request {
|
||||
body: T['body']
|
||||
params: T['params']
|
||||
query: T['query']
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
import express from 'express'
|
||||
import {IRoutes, TMethod} from '@rondo.dev/http-types'
|
||||
import {ITypedRequest} from './ITypedRequest'
|
||||
|
||||
export type TTypedMiddleware<
|
||||
R extends IRoutes,
|
||||
P extends keyof R,
|
||||
M extends TMethod
|
||||
> = (
|
||||
req: ITypedRequest<R[P][M]>,
|
||||
res: express.Response,
|
||||
next: express.NextFunction,
|
||||
) => void
|
||||
|
||||
export type TTypedHandler<
|
||||
R extends IRoutes,
|
||||
P extends keyof R,
|
||||
M extends TMethod
|
||||
> = (
|
||||
req: ITypedRequest<R[P][M]>,
|
||||
res: express.Response,
|
||||
next: express.NextFunction,
|
||||
) => Promise<R[P][M]['response']>
|
||||
@ -1,16 +1,16 @@
|
||||
import { Method, Routes } from '@rondo.dev/http-types'
|
||||
import express from 'express'
|
||||
import {AsyncRouter} from './AsyncRouter'
|
||||
import {IRoutes, TMethod} from '@rondo.dev/http-types'
|
||||
import {ITransactionManager} from '../database/ITransactionManager'
|
||||
import {TTypedHandler} from './TTypedHandler'
|
||||
import { TransactionManager } from '../database/TransactionManager'
|
||||
import { AsyncRouter } from './AsyncRouter'
|
||||
import { TypedHandler } from './TypedHandler'
|
||||
|
||||
export class TransactionalRouter<R extends IRoutes> extends AsyncRouter<R> {
|
||||
constructor(readonly transactionManager: ITransactionManager) {
|
||||
export class TransactionalRouter<R extends Routes> extends AsyncRouter<R> {
|
||||
constructor(readonly transactionManager: TransactionManager) {
|
||||
super()
|
||||
}
|
||||
|
||||
protected wrapHandler<M extends TMethod, P extends keyof R & string>(
|
||||
handler: TTypedHandler<R, P, M>,
|
||||
protected wrapHandler<M extends Method, P extends keyof R & string>(
|
||||
handler: TypedHandler<R, P, M>,
|
||||
): express.RequestHandler {
|
||||
return async (req, res, next) => {
|
||||
await this.transactionManager
|
||||
|
||||
23
packages/server/src/router/TypedHandler.ts
Normal file
23
packages/server/src/router/TypedHandler.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Method, Routes } from '@rondo.dev/http-types'
|
||||
import express from 'express'
|
||||
import { TypedRequest } from './TypedRequest'
|
||||
|
||||
export type TypedMiddleware<
|
||||
R extends Routes,
|
||||
P extends keyof R,
|
||||
M extends Method
|
||||
> = (
|
||||
req: TypedRequest<R[P][M]>,
|
||||
res: express.Response,
|
||||
next: express.NextFunction,
|
||||
) => void
|
||||
|
||||
export type TypedHandler<
|
||||
R extends Routes,
|
||||
P extends keyof R,
|
||||
M extends Method
|
||||
> = (
|
||||
req: TypedRequest<R[P][M]>,
|
||||
res: express.Response,
|
||||
next: express.NextFunction,
|
||||
) => Promise<R[P][M]['response']>
|
||||
8
packages/server/src/router/TypedRequest.ts
Normal file
8
packages/server/src/router/TypedRequest.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Route } from '@rondo.dev/http-types'
|
||||
import express from 'express'
|
||||
|
||||
export interface TypedRequest<T extends Route> extends express.Request {
|
||||
body: T['body']
|
||||
params: T['params']
|
||||
query: T['query']
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
export * from './AsyncRouter'
|
||||
export * from './TTypedHandler'
|
||||
export * from './ITypedRequest'
|
||||
export * from './TransactionalRouter'
|
||||
export * from './TypedHandler'
|
||||
export * from './TypedRequest'
|
||||
|
||||
@ -1,53 +0,0 @@
|
||||
import {AsyncRouter} from '../router'
|
||||
import {BaseRoute} from './BaseRoute'
|
||||
import {IAPIDef} from '@rondo.dev/common'
|
||||
import {IAuthService} from '../services'
|
||||
import {Authenticator} from '../middleware'
|
||||
import {ensureLoggedInApi} from '../middleware'
|
||||
|
||||
export class AuthRoutes extends BaseRoute<IAPIDef> {
|
||||
constructor(
|
||||
protected readonly authService: IAuthService,
|
||||
protected readonly authenticator: Authenticator,
|
||||
protected readonly t: AsyncRouter<IAPIDef>,
|
||||
) {
|
||||
super(t)
|
||||
}
|
||||
|
||||
setup(t: AsyncRouter<IAPIDef>) {
|
||||
t.post('/auth/register', async (req, res) => {
|
||||
const user = await this.authService.createUser({
|
||||
username: req.body.username,
|
||||
password: req.body.password,
|
||||
firstName: req.body.firstName,
|
||||
lastName: req.body.lastName,
|
||||
})
|
||||
await req.logInPromise(user)
|
||||
return user
|
||||
})
|
||||
|
||||
t.post('/auth/login', async (req, res, next) => {
|
||||
const user = await this.authenticator
|
||||
.authenticate('local')(req, res, next)
|
||||
|
||||
if (!user) {
|
||||
res.status(401)
|
||||
return
|
||||
}
|
||||
await req.logInPromise(user)
|
||||
return user
|
||||
})
|
||||
|
||||
t.post('/auth/password', [ensureLoggedInApi], async req => {
|
||||
await this.authService.changePassword({
|
||||
userId: req.user!.id,
|
||||
oldPassword: req.body.oldPassword,
|
||||
newPassword: req.body.newPassword,
|
||||
})
|
||||
})
|
||||
|
||||
t.get('/auth/logout', async (req, res) => {
|
||||
req.logout()
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
import {THandler} from '../middleware/THandler'
|
||||
import {AsyncRouter} from '../router'
|
||||
import {IRoutes} from '@rondo.dev/http-types'
|
||||
|
||||
export abstract class BaseRoute<T extends IRoutes> {
|
||||
readonly handle: THandler
|
||||
|
||||
constructor(protected readonly t: AsyncRouter<T>) {
|
||||
this.handle = t.router
|
||||
this.setup(t)
|
||||
}
|
||||
|
||||
protected abstract setup(t: AsyncRouter<T>): void
|
||||
}
|
||||
46
packages/server/src/routes/configureAuthRoutes.ts
Normal file
46
packages/server/src/routes/configureAuthRoutes.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { APIDef, AuthService } from '@rondo.dev/common'
|
||||
import { Authenticator, ensureLoggedInApi } from '../middleware'
|
||||
import { AsyncRouter } from '../router'
|
||||
|
||||
export function configureAuthRoutes(
|
||||
authService: AuthService,
|
||||
authenticator: Authenticator,
|
||||
t: AsyncRouter<APIDef>,
|
||||
) {
|
||||
t.post('/auth/register', async (req, res) => {
|
||||
const user = await authService.createUser({
|
||||
username: req.body.username,
|
||||
password: req.body.password,
|
||||
firstName: req.body.firstName,
|
||||
lastName: req.body.lastName,
|
||||
})
|
||||
await req.logInPromise(user)
|
||||
return user
|
||||
})
|
||||
|
||||
t.post('/auth/login', async (req, res, next) => {
|
||||
const user = await authenticator
|
||||
.authenticate('local')(req, res, next)
|
||||
|
||||
if (!user) {
|
||||
res.status(401)
|
||||
return
|
||||
}
|
||||
await req.logInPromise(user)
|
||||
return user
|
||||
})
|
||||
|
||||
t.post('/auth/password', [ensureLoggedInApi], async req => {
|
||||
await authService.changePassword({
|
||||
userId: req.user!.id,
|
||||
oldPassword: req.body.oldPassword,
|
||||
newPassword: req.body.newPassword,
|
||||
})
|
||||
})
|
||||
|
||||
t.get('/auth/logout', async (req, res) => {
|
||||
req.logout()
|
||||
})
|
||||
|
||||
return t.router
|
||||
}
|
||||
@ -1,3 +1,2 @@
|
||||
export * from './application'
|
||||
export * from './AuthRoutes'
|
||||
export * from './BaseRoute'
|
||||
export * from './configureAuthRoutes'
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { IContext } from '@rondo.dev/common'
|
||||
import { Context } from '@rondo.dev/common'
|
||||
import { WithContext, ensure } from '@rondo.dev/jsonrpc'
|
||||
|
||||
export { IContext }
|
||||
export type RPC<Service> = WithContext<Service, IContext>
|
||||
export { Context }
|
||||
export type RPC<Service> = WithContext<Service, Context>
|
||||
|
||||
export const ensureLoggedIn = ensure<IContext>(
|
||||
export const ensureLoggedIn = ensure<Context>(
|
||||
c => !!c.user && !!c.user.id,
|
||||
'You must be logged in to perform this action',
|
||||
)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { TeamServiceMethods, ITeamService } from '@rondo.dev/common'
|
||||
import { TeamServiceMethods, TeamService } from '@rondo.dev/common'
|
||||
import { test } from '../test'
|
||||
|
||||
describe('team', () => {
|
||||
@ -15,7 +15,7 @@ describe('team', () => {
|
||||
})
|
||||
|
||||
const getClient = () =>
|
||||
test.rpc<ITeamService>(
|
||||
test.rpc<TeamService>(
|
||||
'/rpc/teamService',
|
||||
TeamServiceMethods,
|
||||
headers,
|
||||
@ -1,20 +1,19 @@
|
||||
import { ITeamAddUserParams, ITeamCreateParams, ITeamRemoveParams, ITeamService, ITeamUpdateParams, trim } from '@rondo.dev/common'
|
||||
import { IUserInTeam } from '@rondo.dev/common/lib/team/IUserInTeam'
|
||||
import { TeamAddUserParams, TeamCreateParams, TeamRemoveParams, TeamService, TeamUpdateParams, trim, UserPermissions } from '@rondo.dev/common'
|
||||
import { UserInTeam } from '@rondo.dev/common/lib/team/UserInTeam'
|
||||
import Validator from '@rondo.dev/validator'
|
||||
import { IDatabase } from '../database/IDatabase'
|
||||
import { Database } from '../database/Database'
|
||||
import { Team } from '../entities/Team'
|
||||
import { UserTeam } from '../entities/UserTeam'
|
||||
import { IUserPermissions } from '../services/IUserPermissions'
|
||||
import { ensureLoggedIn, IContext, RPC } from './RPC'
|
||||
import { ensureLoggedIn, Context, RPC } from './RPC'
|
||||
|
||||
@ensureLoggedIn
|
||||
export class TeamService implements RPC<ITeamService> {
|
||||
export class SQLTeamService implements RPC<TeamService> {
|
||||
constructor(
|
||||
protected readonly db: IDatabase,
|
||||
protected readonly permissions: IUserPermissions,
|
||||
protected readonly db: Database,
|
||||
protected readonly permissions: UserPermissions,
|
||||
) {}
|
||||
|
||||
async create(context: IContext, params: ITeamCreateParams) {
|
||||
async create(context: Context, params: TeamCreateParams) {
|
||||
const userId = context.user!.id
|
||||
const name = trim(params.name)
|
||||
|
||||
@ -38,7 +37,7 @@ export class TeamService implements RPC<ITeamService> {
|
||||
return (await this.findOne(context, team.id))!
|
||||
}
|
||||
|
||||
async remove(context: IContext, {id}: ITeamRemoveParams) {
|
||||
async remove(context: Context, {id}: TeamRemoveParams) {
|
||||
const userId = context.user!.id
|
||||
|
||||
await this.permissions.belongsToTeam({
|
||||
@ -55,7 +54,7 @@ export class TeamService implements RPC<ITeamService> {
|
||||
return {id}
|
||||
}
|
||||
|
||||
async update(context: IContext, {id, name}: ITeamUpdateParams) {
|
||||
async update(context: Context, {id, name}: TeamUpdateParams) {
|
||||
const userId = context.user!.id
|
||||
|
||||
await this.permissions.belongsToTeam({
|
||||
@ -73,7 +72,7 @@ export class TeamService implements RPC<ITeamService> {
|
||||
return (await this.findOne(context, id))!
|
||||
}
|
||||
|
||||
async addUser(context: IContext, params: ITeamAddUserParams) {
|
||||
async addUser(context: Context, params: TeamAddUserParams) {
|
||||
const {userId, teamId, roleId} = params
|
||||
await this.db.getRepository(UserTeam)
|
||||
.save({userId, teamId, roleId})
|
||||
@ -89,7 +88,7 @@ export class TeamService implements RPC<ITeamService> {
|
||||
return this._mapUserInTeam(userTeam!)
|
||||
}
|
||||
|
||||
async removeUser(context: IContext, params: ITeamAddUserParams) {
|
||||
async removeUser(context: Context, params: TeamAddUserParams) {
|
||||
const {teamId, userId, roleId} = params
|
||||
|
||||
await this.permissions.belongsToTeam({
|
||||
@ -104,11 +103,11 @@ export class TeamService implements RPC<ITeamService> {
|
||||
return {teamId, userId, roleId}
|
||||
}
|
||||
|
||||
async findOne(context: IContext, id: number) {
|
||||
async findOne(context: Context, id: number) {
|
||||
return this.db.getRepository(Team).findOne(id)
|
||||
}
|
||||
|
||||
async find(context: IContext) {
|
||||
async find(context: Context) {
|
||||
const userId = context.user!.id
|
||||
|
||||
return this.db.getRepository(Team)
|
||||
@ -119,7 +118,7 @@ export class TeamService implements RPC<ITeamService> {
|
||||
.getMany()
|
||||
}
|
||||
|
||||
async findUsers(context: IContext, teamId: number) {
|
||||
async findUsers(context: Context, teamId: number) {
|
||||
const userId = context.user!.id
|
||||
|
||||
await this.permissions.belongsToTeam({
|
||||
@ -139,7 +138,7 @@ export class TeamService implements RPC<ITeamService> {
|
||||
}
|
||||
}
|
||||
|
||||
protected _mapUserInTeam(ut: UserTeam): IUserInTeam {
|
||||
protected _mapUserInTeam(ut: UserTeam): UserInTeam {
|
||||
return {
|
||||
teamId: ut.teamId,
|
||||
userId: ut.userId,
|
||||
@ -1,7 +1,7 @@
|
||||
import {test} from '../test'
|
||||
import { IUserService, UserServiceMethods } from '@rondo.dev/common'
|
||||
import { UserService, UserServiceMethods } from '@rondo.dev/common'
|
||||
|
||||
describe('user', () => {
|
||||
describe('SQLUserService', () => {
|
||||
|
||||
test.withDatabase()
|
||||
|
||||
@ -12,7 +12,7 @@ describe('user', () => {
|
||||
})
|
||||
|
||||
const createService = () => {
|
||||
return test.rpc<IUserService>(
|
||||
return test.rpc<UserService>(
|
||||
'/rpc/userService',
|
||||
UserServiceMethods,
|
||||
headers,
|
||||
@ -1,19 +1,18 @@
|
||||
import { IUserService } from '@rondo.dev/common'
|
||||
import { compare, hash } from 'bcrypt'
|
||||
import createError from 'http-errors'
|
||||
import { IDatabase } from '../database/IDatabase'
|
||||
import { UserService } from '@rondo.dev/common'
|
||||
import { hash } from 'bcrypt'
|
||||
import { Database } from '../database/Database'
|
||||
import { User } from '../entities/User'
|
||||
import { UserEmail } from '../entities/UserEmail'
|
||||
import { ensureLoggedIn, IContext, RPC } from './RPC'
|
||||
import { Context, ensureLoggedIn, RPC } from './RPC'
|
||||
|
||||
const SALT_ROUNDS = 10
|
||||
const MIN_PASSWORD_LENGTH = 10
|
||||
// const MIN_PASSWORD_LENGTH = 10
|
||||
|
||||
@ensureLoggedIn
|
||||
export class UserService implements RPC<IUserService> {
|
||||
constructor(protected readonly db: IDatabase) {}
|
||||
export class SQLUserService implements RPC<UserService> {
|
||||
constructor(protected readonly db: Database) {}
|
||||
|
||||
async getProfile(context: IContext) {
|
||||
async getProfile(context: Context) {
|
||||
const userId = context.user!.id
|
||||
|
||||
// current user should always exist in the database
|
||||
@ -29,7 +28,7 @@ export class UserService implements RPC<IUserService> {
|
||||
}
|
||||
}
|
||||
|
||||
async findUserByEmail(context: IContext, email: string) {
|
||||
async findUserByEmail(context: Context, email: string) {
|
||||
const userEmail = await this.db.getRepository(UserEmail)
|
||||
.findOne({ email }, {
|
||||
relations: ['user'],
|
||||
@ -1,3 +1,3 @@
|
||||
export * from './RPC'
|
||||
export * from './TeamService'
|
||||
export * from './UserService'
|
||||
export * from './SQLTeamService'
|
||||
export * from './SQLUserService'
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
import {ICredentials, INewUser, IUser} from '@rondo.dev/common'
|
||||
|
||||
export interface IAuthService {
|
||||
createUser(credentials: INewUser): Promise<IUser>
|
||||
changePassword(params: {
|
||||
userId: number,
|
||||
oldPassword: string,
|
||||
newPassword: string,
|
||||
}): Promise<any>
|
||||
validateCredentials(credentials: ICredentials): Promise<IUser | undefined>
|
||||
findOne(id: number): Promise<IUser | undefined>
|
||||
findUserByEmail(email: string): Promise<IUser | undefined>
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
export interface IUserPermissions {
|
||||
// TODO check for role too
|
||||
belongsToTeam(params: {userId: number, teamId: number}): Promise<void>
|
||||
}
|
||||
@ -1,14 +1,14 @@
|
||||
import {test} from '../test'
|
||||
import {AuthService} from './AuthService'
|
||||
import { test } from '../test'
|
||||
import { SQLAuthService } from './SQLAuthService'
|
||||
|
||||
describe('AuthService', () => {
|
||||
describe('SQLAuthService', () => {
|
||||
|
||||
test.withDatabase()
|
||||
|
||||
const username = test.username
|
||||
const password = '1234567890'
|
||||
|
||||
const authService = new AuthService(test.bootstrap.database)
|
||||
const authService = new SQLAuthService(test.bootstrap.database)
|
||||
|
||||
async function createUser(u = username, p = password) {
|
||||
return authService.createUser({
|
||||
@ -1,20 +1,19 @@
|
||||
import { ICredentials, INewUser, IUser, trim } from '@rondo.dev/common'
|
||||
import { AuthService, Credentials, NewUser, UserProfile, trim } from '@rondo.dev/common'
|
||||
import Validator from '@rondo.dev/validator'
|
||||
import { compare, hash } from 'bcrypt'
|
||||
import { validate as validateEmail } from 'email-validator'
|
||||
import createError from 'http-errors'
|
||||
import { IDatabase } from '../database/IDatabase'
|
||||
import { Database } from '../database/Database'
|
||||
import { User } from '../entities/User'
|
||||
import { UserEmail } from '../entities/UserEmail'
|
||||
import { IAuthService } from './IAuthService'
|
||||
|
||||
const SALT_ROUNDS = 10
|
||||
const MIN_PASSWORD_LENGTH = 10
|
||||
|
||||
export class AuthService implements IAuthService {
|
||||
constructor(protected readonly db: IDatabase) {}
|
||||
export class SQLAuthService implements AuthService {
|
||||
constructor(protected readonly db: Database) {}
|
||||
|
||||
async createUser(payload: INewUser): Promise<IUser> {
|
||||
async createUser(payload: NewUser): Promise<UserProfile> {
|
||||
const newUser = {
|
||||
username: trim(payload.username),
|
||||
firstName: trim(payload.firstName),
|
||||
@ -88,9 +87,9 @@ export class AuthService implements IAuthService {
|
||||
}
|
||||
|
||||
async changePassword(params: {
|
||||
userId: number,
|
||||
oldPassword: string,
|
||||
newPassword: string,
|
||||
userId: number
|
||||
oldPassword: string
|
||||
newPassword: string
|
||||
}) {
|
||||
const {userId, oldPassword, newPassword} = params
|
||||
const userRepository = this.db.getRepository(User)
|
||||
@ -109,7 +108,7 @@ export class AuthService implements IAuthService {
|
||||
.update(userId, { password })
|
||||
}
|
||||
|
||||
async validateCredentials(credentials: ICredentials) {
|
||||
async validateCredentials(credentials: Credentials) {
|
||||
const {username, password} = credentials
|
||||
const user = await this.db.getRepository(User)
|
||||
.createQueryBuilder('user')
|
||||
@ -1,10 +1,10 @@
|
||||
import { UserPermissions } from '@rondo.dev/common'
|
||||
import createError from 'http-errors'
|
||||
import {IDatabase} from '../database/IDatabase'
|
||||
import {UserTeam} from '../entities/UserTeam'
|
||||
import {IUserPermissions} from './IUserPermissions'
|
||||
import { Database } from '../database/Database'
|
||||
import { UserTeam } from '../entities/UserTeam'
|
||||
|
||||
export class UserPermissions implements IUserPermissions {
|
||||
constructor(protected readonly db: IDatabase) {}
|
||||
export class SQLUserPermissions implements UserPermissions {
|
||||
constructor(protected readonly db: Database) {}
|
||||
|
||||
async belongsToTeam(params: {userId: number, teamId: number}) {
|
||||
const {userId, teamId} = params
|
||||
@ -1,4 +1,2 @@
|
||||
export * from './IAuthService'
|
||||
export * from './AuthService'
|
||||
export * from './IUserPermissions'
|
||||
export * from './UserPermissions'
|
||||
export * from './SQLAuthService'
|
||||
export * from './SQLUserPermissions'
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export interface ISession {
|
||||
export interface DefaultSession {
|
||||
// TODO use timestamp field
|
||||
expiredAt: number
|
||||
id: string
|
||||
@ -1,16 +1,13 @@
|
||||
import express, {Application} from 'express'
|
||||
import request from 'supertest'
|
||||
import {SessionStore} from './SessionStore'
|
||||
import {ISession} from './ISession'
|
||||
import ExpressSession from 'express-session'
|
||||
import loggerFactory from '@rondo.dev/logger'
|
||||
import {
|
||||
createConnection, Column, Connection, Entity, Index, PrimaryColumn,
|
||||
Repository,
|
||||
} from 'typeorm'
|
||||
import express, { Application } from 'express'
|
||||
import ExpressSession from 'express-session'
|
||||
import request from 'supertest'
|
||||
import { Column, Connection, createConnection, Entity, Index, PrimaryColumn, Repository } from 'typeorm'
|
||||
import { DefaultSession } from './DefaultSession'
|
||||
import { SessionStore } from './SessionStore'
|
||||
|
||||
@Entity()
|
||||
class Session implements ISession {
|
||||
class SessionEntity implements DefaultSession {
|
||||
@PrimaryColumn()
|
||||
id!: string
|
||||
|
||||
@ -28,15 +25,15 @@ class Session implements ISession {
|
||||
describe('SessionStore', () => {
|
||||
|
||||
let connection!: Connection
|
||||
let repository!: Repository<Session>
|
||||
let repository!: Repository<SessionEntity>
|
||||
beforeEach(async () => {
|
||||
connection = await createConnection({
|
||||
type: 'sqlite',
|
||||
database: ':memory:',
|
||||
entities: [Session],
|
||||
entities: [SessionEntity],
|
||||
synchronize: true,
|
||||
})
|
||||
repository = connection.getRepository(Session)
|
||||
repository = connection.getRepository(SessionEntity)
|
||||
})
|
||||
|
||||
afterEach(() => connection!.close())
|
||||
@ -88,7 +85,7 @@ describe('SessionStore', () => {
|
||||
.expect(200)
|
||||
}
|
||||
|
||||
function getSession(app: Application, cookie: string = '') {
|
||||
function getSession(app: Application, cookie = '') {
|
||||
return request(app)
|
||||
.get('/session')
|
||||
.set('cookie', cookie)
|
||||
|
||||
@ -1,31 +1,31 @@
|
||||
import {Store} from 'express-session'
|
||||
import {ISession} from './ISession'
|
||||
import {Repository, LessThan} from 'typeorm'
|
||||
import {debounce} from '@rondo.dev/tasq'
|
||||
import { ILogger } from '@rondo.dev/logger'
|
||||
import { Logger } from '@rondo.dev/logger'
|
||||
import { debounce } from '@rondo.dev/tasq'
|
||||
import { Store } from 'express-session'
|
||||
import { LessThan, Repository } from 'typeorm'
|
||||
import { DefaultSession } from './DefaultSession'
|
||||
|
||||
type SessionData = Express.SessionData
|
||||
type Callback = (err?: any, session?: SessionData) => void
|
||||
type CallbackErr = (err?: any) => void
|
||||
|
||||
export interface ISessionStoreOptions<S extends ISession> {
|
||||
export interface SessionStoreOptions<S extends DefaultSession> {
|
||||
readonly ttl: number
|
||||
readonly cleanupDelay: number
|
||||
readonly getRepository: TRepositoryFactory<S>
|
||||
readonly logger: ILogger,
|
||||
buildSession(sessionData: SessionData, session: ISession): S
|
||||
readonly getRepository: RepositoryFactory<S>
|
||||
readonly logger: Logger
|
||||
buildSession(sessionData: SessionData, session: DefaultSession): S
|
||||
}
|
||||
|
||||
export type TRepositoryFactory<T> = () => Repository<T>
|
||||
export type RepositoryFactory<T> = () => Repository<T>
|
||||
|
||||
// TODO casting as any because TypeScript complains. Looks like this is a
|
||||
// bug in TypeScript 3.2.2
|
||||
//
|
||||
// https://github.com/typeorm/typeorm/issues/1544
|
||||
// https://github.com/Microsoft/TypeScript/issues/21592
|
||||
export class SessionStore<S extends ISession> extends Store {
|
||||
export class SessionStore<S extends DefaultSession> extends Store {
|
||||
|
||||
protected readonly getRepository: TRepositoryFactory<S>
|
||||
protected readonly getRepository: RepositoryFactory<S>
|
||||
|
||||
readonly cleanup = debounce(async () => {
|
||||
try {
|
||||
@ -43,7 +43,7 @@ export class SessionStore<S extends ISession> extends Store {
|
||||
}, 1000)
|
||||
|
||||
constructor(
|
||||
protected readonly options: ISessionStoreOptions<S>,
|
||||
protected readonly options: SessionStoreOptions<S>,
|
||||
) {
|
||||
super()
|
||||
this.getRepository = options.getRepository
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
export * from './ISession'
|
||||
export * from './DefaultSession'
|
||||
export * from './SessionStore'
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import {Namespace} from 'cls-hooked'
|
||||
import { Namespace } from 'cls-hooked'
|
||||
|
||||
export class NamespaceMock implements Namespace {
|
||||
readonly context: {[key: string]: any} = {}
|
||||
|
||||
@ -3,11 +3,11 @@ import {RequestTester} from './RequestTester'
|
||||
|
||||
describe('RequestTest', () => {
|
||||
|
||||
interface IAPI {
|
||||
interface API {
|
||||
'/test': {
|
||||
'get': {
|
||||
response: {id: number},
|
||||
},
|
||||
response: {id: number}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,14 +18,14 @@ describe('RequestTest', () => {
|
||||
|
||||
describe('constructor', () => {
|
||||
it('creates a blank baseUrl', () => {
|
||||
const t = new RequestTester<IAPI>(app)
|
||||
const t = new RequestTester<API>(app)
|
||||
expect(t.baseUrl).toEqual('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('RequestTester.request', () => {
|
||||
it('creates a response', async () => {
|
||||
const t = new RequestTester<IAPI>(app, '/api')
|
||||
const t = new RequestTester<API>(app, '/api')
|
||||
const result = await t.request('get', '/test')
|
||||
expect(result.body.id).toBe(1)
|
||||
})
|
||||
|
||||
@ -1,29 +1,32 @@
|
||||
import { URLFormatter } from '@rondo.dev/http-client'
|
||||
import { IRoutes, TMethod } from '@rondo.dev/http-types'
|
||||
/* eslint @typescript-eslint/no-explicit-any: 0 */
|
||||
import { Headers, URLFormatter } from '@rondo.dev/http-client'
|
||||
import { Routes, Method } from '@rondo.dev/http-types'
|
||||
import supertest from 'supertest'
|
||||
|
||||
// https://stackoverflow.com/questions/48215950/exclude-property-from-type
|
||||
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
|
||||
|
||||
interface ITest extends Omit<supertest.Test, 'then' | 'catch' | 'finally'> {}
|
||||
interface Test extends Omit<supertest.Test, 'then' | 'catch' | 'finally'> {}
|
||||
|
||||
interface IResponse<
|
||||
R extends IRoutes,
|
||||
interface Response<
|
||||
R extends Routes,
|
||||
P extends keyof R,
|
||||
M extends TMethod,
|
||||
M extends Method,
|
||||
> extends supertest.Response {
|
||||
body: R[P][M]['response']
|
||||
header: {[key: string]: string}
|
||||
}
|
||||
|
||||
interface IRequest<
|
||||
R extends IRoutes,
|
||||
interface Request<
|
||||
R extends Routes,
|
||||
P extends keyof R,
|
||||
M extends TMethod,
|
||||
> extends ITest, Promise<IResponse<R, P, M>> {
|
||||
M extends Method,
|
||||
> extends Test, Promise<Response<R, P, M>> {
|
||||
send(value: R[P][M]['body'] | string): this
|
||||
expect(status: number, body?: any): this
|
||||
expect(body: string | RegExp | object | ((res: Response) => any)): this
|
||||
expect(
|
||||
body: string | RegExp | object | ((res: Response<R, P, M>,
|
||||
) => any)): this
|
||||
expect(field: string, val: string | RegExp): this
|
||||
// any other method that's called will return "this" from supertest's
|
||||
// or superagent's type definition and afterwards the promise will no longer
|
||||
@ -31,22 +34,18 @@ interface IRequest<
|
||||
// type definition
|
||||
}
|
||||
|
||||
interface IRequestOptions<
|
||||
R extends IRoutes,
|
||||
interface RequestOptions<
|
||||
R extends Routes,
|
||||
P extends keyof R,
|
||||
M extends TMethod,
|
||||
M extends Method,
|
||||
> {
|
||||
params?: R[P][M]['params'],
|
||||
query?: R[P][M]['query'],
|
||||
params?: R[P][M]['params']
|
||||
query?: R[P][M]['query']
|
||||
}
|
||||
|
||||
export interface IHeaders {
|
||||
[key: string]: string
|
||||
}
|
||||
export class RequestTester<R extends Routes> {
|
||||
|
||||
export class RequestTester<R extends IRoutes> {
|
||||
|
||||
protected headers: IHeaders = {}
|
||||
protected headers: Headers = {}
|
||||
protected formatter: URLFormatter = new URLFormatter()
|
||||
|
||||
constructor(
|
||||
@ -54,15 +53,14 @@ export class RequestTester<R extends IRoutes> {
|
||||
readonly baseUrl = '',
|
||||
) {}
|
||||
|
||||
setHeaders(headers: IHeaders): this {
|
||||
setHeaders(headers: Headers): this {
|
||||
this.headers = headers
|
||||
return this
|
||||
}
|
||||
|
||||
request<M extends TMethod, P extends keyof R & string>(
|
||||
method: M, path: P, options: IRequestOptions<R, P, 'post'> = {},
|
||||
)
|
||||
: IRequest<R, P, M> {
|
||||
request<M extends Method, P extends keyof R & string>(
|
||||
method: M, path: P, options: RequestOptions<R, P, 'post'> = {},
|
||||
): Request<R, P, M> {
|
||||
const url = this.formatter.format(path, options.params, options.query)
|
||||
const test = supertest(this.app)[method](`${this.baseUrl}${url}`)
|
||||
Object.keys(this.headers).forEach(key => {
|
||||
@ -73,28 +71,28 @@ export class RequestTester<R extends IRoutes> {
|
||||
|
||||
get<P extends keyof R & string>(
|
||||
path: P,
|
||||
options?: IRequestOptions<R, P, 'get'>,
|
||||
options?: RequestOptions<R, P, 'get'>,
|
||||
) {
|
||||
return this.request('get', path, options)
|
||||
}
|
||||
|
||||
post<P extends keyof R & string>(
|
||||
path: P,
|
||||
options?: IRequestOptions<R, P, 'post'>,
|
||||
options?: RequestOptions<R, P, 'post'>,
|
||||
) {
|
||||
return this.request('post', path, options)
|
||||
}
|
||||
|
||||
put<P extends keyof R & string>(
|
||||
path: P,
|
||||
options?: IRequestOptions<R, P, 'put'>,
|
||||
options?: RequestOptions<R, P, 'put'>,
|
||||
) {
|
||||
return this.request('put', path, options)
|
||||
}
|
||||
|
||||
delete<P extends keyof R & string>(
|
||||
path: P,
|
||||
options?: IRequestOptions<R, P, 'delete'>,
|
||||
options?: RequestOptions<R, P, 'delete'>,
|
||||
) {
|
||||
return this.request('delete', path, options)
|
||||
}
|
||||
|
||||
@ -1,29 +1,26 @@
|
||||
import express from 'express'
|
||||
import supertest from 'supertest'
|
||||
import {Connection, QueryRunner} from 'typeorm'
|
||||
import {
|
||||
ENTITY_MANAGER, ITransactionManager, TRANSACTION_ID,
|
||||
} from '../database/ITransactionManager'
|
||||
import {IRoutes} from '@rondo.dev/http-types'
|
||||
import {IBootstrap} from '../application/IBootstrap'
|
||||
import {RequestTester} from './RequestTester'
|
||||
import {Role} from '../entities/Role'
|
||||
import {CORRELATION_ID} from '../middleware'
|
||||
import shortid from 'shortid'
|
||||
import { AddressInfo } from 'net'
|
||||
/* eslint @typescript-eslint/no-explicit-any: 0 */
|
||||
import { Routes } from '@rondo.dev/http-types'
|
||||
import { createRemoteClient, FunctionPropertyNames, RPCClient } from '@rondo.dev/jsonrpc'
|
||||
import {Server} from 'http'
|
||||
import { IAppServer } from '../application/IAppServer'
|
||||
import { Server } from 'http'
|
||||
import { AddressInfo } from 'net'
|
||||
import shortid from 'shortid'
|
||||
import supertest from 'supertest'
|
||||
import { Connection, QueryRunner } from 'typeorm'
|
||||
import { AppServer } from '../application/AppServer'
|
||||
import { Bootstrap } from '../application/Bootstrap'
|
||||
import { ENTITY_MANAGER, TransactionManager, TRANSACTION_ID } from '../database/TransactionManager'
|
||||
import { Role } from '../entities/Role'
|
||||
import { RequestTester } from './RequestTester'
|
||||
|
||||
export class TestUtils<T extends IRoutes> {
|
||||
export class TestUtils<T extends Routes> {
|
||||
readonly username = this.createTestUsername()
|
||||
readonly password = 'Password10'
|
||||
|
||||
readonly app: IAppServer
|
||||
readonly app: AppServer
|
||||
readonly context: string
|
||||
readonly transactionManager: ITransactionManager
|
||||
readonly transactionManager: TransactionManager
|
||||
|
||||
constructor(readonly bootstrap: IBootstrap) {
|
||||
constructor(readonly bootstrap: Bootstrap) {
|
||||
this.app = bootstrap.application.server
|
||||
this.context = this.bootstrap.getConfig().app.context
|
||||
this.transactionManager = this.bootstrap.database.transactionManager
|
||||
@ -82,7 +79,7 @@ export class TestUtils<T extends IRoutes> {
|
||||
.save({name})
|
||||
}
|
||||
|
||||
async getError(promise: Promise<any>): Promise<Error> {
|
||||
async getError(promise: Promise<unknown>): Promise<Error> {
|
||||
let error!: Error
|
||||
try {
|
||||
await promise
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
export * from './TestUtils'
|
||||
export * from './RequestTester'
|
||||
export * from './NamespaceMock'
|
||||
export * from './RequestTester'
|
||||
export * from './TestUtils'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user