Fix packages/server

This commit is contained in:
Jerko Steiner 2019-09-16 10:46:17 +07:00
parent 92912af839
commit 7631f085ef
101 changed files with 885 additions and 910 deletions

View File

@ -1,3 +1,4 @@
packages/*/lib
packages/*/esm
build/
packages/*/src/migrations

View File

@ -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

View 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>
}

View File

@ -0,0 +1,5 @@
export interface ChangePasswordParams {
userId: number
oldPassword: string
newPassword: string
}

View File

@ -1 +1,3 @@
export * from './AuthService'
export * from './Credentials'
export * from './ChangePasswordParams'

View File

@ -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'

View File

@ -0,0 +1,4 @@
export interface BelongsToTeamParams {
userId: number
teamId: number
}

View File

@ -0,0 +1,6 @@
import {BelongsToTeamParams} from './BelongsToTeamParams'
export interface UserPermissions {
// TODO check for role too
belongsToTeam(params: BelongsToTeamParams): Promise<void>
}

View File

@ -0,0 +1,2 @@
export * from './BelongsToTeamParams'
export * from './UserPermissions'

View File

@ -0,0 +1,3 @@
export * from './BelongsToTeamParams'
export * from './UserPermissions'

View File

@ -0,0 +1,4 @@
extends:
- ../../.eslintrc.yaml
rules:
'@typescript-eslint/no-explicit-any': off

View File

@ -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,
}

View File

@ -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

View File

@ -0,0 +1,7 @@
import { Database } from '../database/Database'
import { AppServer } from './AppServer'
export interface Application {
readonly server: AppServer
readonly database: Database
}

View File

@ -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>
}

View 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
})
}
}

View File

@ -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

View File

@ -1,7 +0,0 @@
import { IDatabase } from '../database/IDatabase'
import { IAppServer } from './IAppServer'
export interface IApplication {
readonly server: IAppServer
readonly database: IDatabase
}

View File

@ -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>
}

View File

@ -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>
}

View File

@ -1,6 +0,0 @@
import { IAuthService, IUserPermissions } from '../services'
export interface IServices {
authService: IAuthService
userPermissions: IUserPermissions
}

View 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>
}

View File

@ -0,0 +1,6 @@
import { AuthService, UserPermissions } from '@rondo.dev/common'
export interface Services {
authService: AuthService
userPermissions: UserPermissions
}

View File

@ -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,
},

View File

@ -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()

View File

@ -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'

View File

@ -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,
})

View File

@ -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'),

View File

@ -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>
}

View File

@ -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>
}

View File

@ -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>
}

View 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)
}
}

View 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)
}
}

View File

@ -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>
}

View File

@ -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)

View File

@ -1,4 +1,4 @@
export * from './ITransactionManager'
export * from './TransactionManager'
export * from './IDatabase'
export * from './SQLTransactionManager'
export * from './Database'
export * from './SQLDatabase'

View File

@ -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,

View File

@ -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 {

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -1,4 +1,4 @@
import {TransformedError} from './TransformedError'
import { TransformedError } from './TransformedError'
export class ErrorTransformer {
constructor(

View File

@ -1,4 +1,4 @@
import {valueOrError} from './'
import { valueOrError } from './'
describe('valueOrError', () => {

View File

@ -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,
) {}

View File

@ -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')

View File

@ -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

View File

@ -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) => {

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,3 @@
import { NextFunction, Request, Response } from 'express'
export type Handler = (req: Request, res: Response, next: NextFunction) => any

View File

@ -1,6 +0,0 @@
import {THandler} from './THandler'
import {TErrorHandler} from './TErrorHandler'
export interface IMiddleware {
handle: THandler | THandler[] | TErrorHandler
}

View File

@ -0,0 +1,6 @@
import {Handler} from './Handler'
import {ErrorHandler} from './ErrorHandler'
export interface Middleware {
handle: Handler | Handler[] | ErrorHandler
}

View File

@ -0,0 +1,4 @@
import { NextFunction, Request, Response } from 'express'
export type PromiseHandler<T> =
(req: Request, res: Response, next: NextFunction) => Promise<T>

View File

@ -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', () => {

View File

@ -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 }
}

View File

@ -1,3 +0,0 @@
import {Request, Response, NextFunction} from 'express'
export type THandler = (req: Request, res: Response, next: NextFunction) => any

View File

@ -1,4 +0,0 @@
import {Request, Response, NextFunction} from 'express'
export type TPromiseHandler<T> =
(req: Request, res: Response, next: NextFunction) => Promise<T>

View File

@ -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)

View File

@ -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} = {}

View File

@ -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)

View File

@ -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

View File

@ -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'

View File

@ -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()
})

View File

@ -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)

View File

@ -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']
}

View File

@ -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']>

View File

@ -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

View 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']>

View 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']
}

View File

@ -1,4 +1,4 @@
export * from './AsyncRouter'
export * from './TTypedHandler'
export * from './ITypedRequest'
export * from './TransactionalRouter'
export * from './TypedHandler'
export * from './TypedRequest'

View File

@ -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()
})
}
}

View File

@ -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
}

View 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
}

View File

@ -1,3 +1,2 @@
export * from './application'
export * from './AuthRoutes'
export * from './BaseRoute'
export * from './configureAuthRoutes'

View File

@ -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',
)

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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'],

View File

@ -1,3 +1,3 @@
export * from './RPC'
export * from './TeamService'
export * from './UserService'
export * from './SQLTeamService'
export * from './SQLUserService'

View File

@ -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>
}

View File

@ -1,4 +0,0 @@
export interface IUserPermissions {
// TODO check for role too
belongsToTeam(params: {userId: number, teamId: number}): Promise<void>
}

View File

@ -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({

View File

@ -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')

View File

@ -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

View File

@ -1,4 +1,2 @@
export * from './IAuthService'
export * from './AuthService'
export * from './IUserPermissions'
export * from './UserPermissions'
export * from './SQLAuthService'
export * from './SQLUserPermissions'

View File

@ -1,4 +1,4 @@
export interface ISession {
export interface DefaultSession {
// TODO use timestamp field
expiredAt: number
id: string

View File

@ -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)

View File

@ -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

View File

@ -1,2 +1,2 @@
export * from './ISession'
export * from './DefaultSession'
export * from './SessionStore'

View File

@ -1,4 +1,4 @@
import {Namespace} from 'cls-hooked'
import { Namespace } from 'cls-hooked'
export class NamespaceMock implements Namespace {
readonly context: {[key: string]: any} = {}

View File

@ -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)
})

View File

@ -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)
}

View File

@ -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

View File

@ -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