Extract packages/server/src/database to packages/{db,db-typeorm}

This commit is contained in:
Jerko Steiner 2019-09-18 12:43:46 +07:00
parent c8d22278a5
commit 9c9c41aeaf
44 changed files with 242 additions and 122 deletions

View File

@ -10,13 +10,15 @@
- [ ] Add React error boundaries
- [ ] Use strings as ids for big decimals
- [ ] Integrate Google (and other social fb/twitter) logins
- [ ] Framewor development
- [ ] Framework development
- [ ] Improve comments
- [ ] Generate docs using using `typedoc`
- [ ] Generate framework website using Docusaurus
- [ ] Split framework projects and actual projects
- [ ] Experiment with styled components
- [ ] Replace tslint with eslint:
- [ ] Use JSON schema instead of @Entity decorators
- [x] Extract database into a separate module
- [x] Replace tslint with eslint:
https://github.com/typescript-eslint/typescript-eslint
# JSONRPC

View File

@ -17,7 +17,9 @@
"@rondo.dev/server": "file:packages/server",
"@rondo.dev/tasq": "file:packages/tasq",
"@rondo.dev/test-utils": "file:packages/test-utils",
"@rondo.dev/validator": "file:packages/validator"
"@rondo.dev/validator": "file:packages/validator",
"@rondo.dev/db": "file:packages/db",
"@rondo.dev/db-typeorm": "file:packages/db-typeorm"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^2.2.0",
@ -101,4 +103,4 @@
"watchify": "^3.11.1"
},
"name": "node"
}
}

View File

@ -0,0 +1,16 @@
module.exports = {
roots: [
'<rootDir>/src',
],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
testRegex: '(/__tests__/.*|\\.(test|spec))\\.tsx?$',
moduleFileExtensions: [
'ts',
'tsx',
'js',
'jsx',
],
setupFiles: ['<rootDir>/jest.setup.js'],
}

View File

@ -0,0 +1,4 @@
if (!process.env.LOG) {
process.env.LOG = 'sql:warn'
}
process.chdir(__dirname)

4
packages/db-typeorm/package-lock.json generated Normal file
View File

@ -0,0 +1,4 @@
{
"name": "@rondo.dev/db-typeorm",
"lockfileVersion": 1
}

View File

@ -0,0 +1,14 @@
{
"name": "@rondo.dev/db-typeorm",
"private": true,
"scripts": {
"test": "jest",
"lint": "tslint --project .",
"compile": "tsc",
"clean": "rm -rf lib/"
},
"dependencies": {},
"main": "lib/index.js",
"module": "esm/index.js",
"types": "lib/index.d.ts"
}

View File

@ -1,19 +1,20 @@
import { Database } from '@rondo.dev/db'
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'
import { Connection, ConnectionOptions, createConnection, EntityManager, EntitySchema, Logger, ObjectType, Repository } from 'typeorm'
import { TypeORMTransactionManager } from './TypeORMTransactionManager'
export class SQLDatabase implements Database {
export class TypeORMDatabase implements Database<
Connection, EntityManager, TypeORMTransactionManager>
{
protected connection?: Connection
transactionManager: TransactionManager
transactionManager: TypeORMTransactionManager
constructor(
readonly namespace: Namespace,
protected readonly logger: Logger,
protected readonly options: ConnectionOptions,
) {
this.transactionManager = new SQLTransactionManager(
this.transactionManager = new TypeORMTransactionManager(
namespace,
this.getConnection,
)
@ -49,3 +50,4 @@ export class SQLDatabase implements Database {
}
}

View File

@ -1,16 +1,15 @@
import { CORRELATION_ID, TRANSACTION_ID } from '@rondo.dev/db'
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'
import { Logger as TLogger, QueryRunner } from 'typeorm'
export class SQLLogger implements TypeORMLogger {
export class TypeORMLogger implements TLogger {
constructor(
protected readonly logger: Logger,
protected readonly ns: Namespace,
) {}
logQuery(query: string, parameters?: any[], queryRunner?: QueryRunner): any {
logQuery(query: string, parameters?: unknown[], queryRunner?: QueryRunner) {
const correlationId = this.getCorrelationId()
if (parameters) {
this.logger.info('%s %s -- %s', correlationId, query, parameters)
@ -24,7 +23,7 @@ export class SQLLogger implements TypeORMLogger {
logQueryError(
error: string,
query: string,
parameters?: any[],
parameters?: unknown[],
queryRunner?: QueryRunner,
) {
const correlationId = this.getCorrelationId()
@ -40,7 +39,7 @@ export class SQLLogger implements TypeORMLogger {
*/
logQuerySlow(
time: number, query: string,
parameters?: any[],
parameters?: unknown[],
queryRunner?: QueryRunner,
) {
const correlationId = this.getCorrelationId()
@ -70,7 +69,7 @@ export class SQLLogger implements TypeORMLogger {
*/
log(
level: 'log' | 'info' | 'warn',
message: any,
message: unknown,
queryRunner?: QueryRunner,
) {
const correlationId = this.getCorrelationId()

View File

@ -1,21 +1,22 @@
import { TRANSACTION, TransactionManager, TRANSACTION_ID } from '@rondo.dev/db'
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 {
export class TypeORMTransactionManager
implements TransactionManager<EntityManager> {
constructor(
readonly ns: Namespace,
readonly getConnection: GetConnection,
) {}
getEntityManager = (): EntityManager => {
const entityManager = this.ns.get(ENTITY_MANAGER) as EntityManager
const entityManager = this.ns.get(TRANSACTION) as EntityManager
if (entityManager) {
return entityManager
}
@ -29,7 +30,7 @@ export class SQLTransactionManager implements TransactionManager {
}
isInTransaction = (): boolean => {
return !!this.ns.get(ENTITY_MANAGER)
return !!this.ns.get(TRANSACTION)
}
async doInTransaction<T>(fn: (em: EntityManager) => Promise<T>) {
@ -67,6 +68,6 @@ export class SQLTransactionManager implements TransactionManager {
}
protected setEntityManager(entityManager: EntityManager | undefined) {
this.ns.set(ENTITY_MANAGER, entityManager)
this.ns.set(TRANSACTION, entityManager)
}
}

View File

@ -0,0 +1,3 @@
export * from './TypeORMDatabase'
export * from './TypeORMLogger'
export * from './TypeORMTransactionManager'

View File

@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "esm"
},
"references": [
{
"path": "../db/tsconfig.esm.json"
},
{
"path": "../logger/tsconfig.esm.json"
}
]
}

View File

@ -0,0 +1,11 @@
{
"extends": "../tsconfig.common.json",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"references": [
{"path": "../db"},
{"path": "../logger"}
]
}

View File

@ -0,0 +1,16 @@
module.exports = {
roots: [
'<rootDir>/src',
],
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
testRegex: '(/__tests__/.*|\\.(test|spec))\\.tsx?$',
moduleFileExtensions: [
'ts',
'tsx',
'js',
'jsx',
],
setupFiles: ['<rootDir>/jest.setup.js'],
}

View File

@ -0,0 +1,4 @@
if (!process.env.LOG) {
process.env.LOG = 'sql:warn'
}
process.chdir(__dirname)

4
packages/db/package-lock.json generated Normal file
View File

@ -0,0 +1,4 @@
{
"name": "@rondo.dev/db",
"lockfileVersion": 1
}

14
packages/db/package.json Normal file
View File

@ -0,0 +1,14 @@
{
"name": "@rondo.dev/db",
"private": true,
"scripts": {
"test": "jest",
"lint": "tslint --project .",
"compile": "tsc",
"clean": "rm -rf lib/"
},
"dependencies": {},
"main": "lib/index.js",
"module": "esm/index.js",
"types": "lib/index.d.ts"
}

View File

@ -0,0 +1,14 @@
import { Namespace } from "cls-hooked";
import { TransactionManager } from "./TransactionManager";
export interface Database<
Connection,
Transaction,
TM extends TransactionManager<Transaction>,
> {
namespace: Namespace
transactionManager: TM
connect(): Promise<Connection>
getConnection(): Connection
close(): Promise<void>
}

View File

@ -0,0 +1,12 @@
export interface TransactionManager<Transaction = unknown> {
isInTransaction: () => boolean
/**
* Start a new or reuse an existing transaction.
*/
doInTransaction: <T>(fn: (t: Transaction) => Promise<T>) => Promise<T>
/**
* Always start a new transaction, regardless if there is one already in
* progress in the current context.
*/
doInNewTransaction: <T>(fn: (t: Transaction) => Promise<T>) => Promise<T>
}

View File

@ -0,0 +1,4 @@
export const CORRELATION_ID = 'CORRELATION_ID'
export const TRANSACTION_ID = 'TRANSACTION_ID'
export const TRANSACTION = 'TRANSACTION'

3
packages/db/src/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './constants'
export * from './Database'
export * from './TransactionManager'

View File

@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "esm"
},
"references": []
}

View File

@ -0,0 +1,9 @@
{
"extends": "../tsconfig.common.json",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"references": [
]
}

View File

@ -19,9 +19,9 @@ app:
migrationsRun: false
logging: true
entities:
- src/entities/*.ts
- src/entities/index.ts
migrations:
- src/migrations/*.ts
- src/migrations/index.ts
cli:
migrationsDir: src/migrations
entitiesDir: src/entities

View File

@ -1,7 +1,7 @@
import { Database } from '../database/Database'
import { AppServer } from './AppServer'
import { TypeORMDatabase } from '@rondo.dev/db-typeorm';
export interface Application {
readonly server: AppServer
readonly database: Database
readonly database: TypeORMDatabase
}

View File

@ -1,11 +1,11 @@
import {AddressInfo} from 'net'
import {Application} from './Application'
import {Database} from '../database/Database'
import {Config} from './Config'
import { AddressInfo } from 'net'
import { Application } from './Application'
import { Config } from './Config'
import { TypeORMDatabase } from '@rondo.dev/db-typeorm'
export interface Bootstrap {
readonly application: Application
readonly database: Database
readonly database: TypeORMDatabase
getConfig(): Config
listen(port?: number | string, hostname?: string): Promise<void>
getAddress(): AddressInfo | string

View File

@ -2,13 +2,13 @@ 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 { loggerFactory } from '../logger'
import { Application } from './Application'
import { Bootstrap } from './Bootstrap'
import { Config } from './Config'
import { ServerConfigurator } from './configureServer'
import { createServer } from './createServer'
import { TypeORMDatabase, TypeORMLogger } from '@rondo.dev/db-typeorm'
export interface ServerBootstrapParams {
readonly config: Config
@ -35,7 +35,7 @@ export class ServerBootstrap implements Bootstrap {
protected server?: Server
protected inUse = false
readonly application: Application
readonly database: Database
readonly database: TypeORMDatabase
constructor(params: ServerBootstrapParams) {
this.config = {
@ -65,13 +65,14 @@ export class ServerBootstrap implements Bootstrap {
return this.config
}
protected createDatabase(): Database {
protected createDatabase(): TypeORMDatabase {
const {namespace} = this
const sqlLogger = new SQLLogger(loggerFactory.getLogger('sql'), namespace)
return new SQLDatabase(namespace, sqlLogger, this.getConfig().app.db)
const sqlLogger = new TypeORMLogger(
loggerFactory.getLogger('sql'), namespace)
return new TypeORMDatabase(namespace, sqlLogger, this.getConfig().app.db)
}
protected createApplication(database: Database): Application {
protected createApplication(database: TypeORMDatabase): Application {
const {configureServer} = this
return createServer(configureServer(this.getConfig(), database))
}

View File

@ -1,8 +1,8 @@
import { Config } from './Config'
import { Database } from '../database'
import { TypeORMDatabase } from '@rondo.dev/db-typeorm'
import { Logger } from '@rondo.dev/logger'
import { ErrorRequestHandler, RequestHandlerParams } from 'express-serve-static-core'
import { Config } from './Config'
import { Services } from './Services'
import { RequestHandlerParams, ErrorRequestHandler } from 'express-serve-static-core'
export interface ServerMiddleware {
path: string
@ -12,7 +12,7 @@ export interface ServerMiddleware {
export interface ServerConfig {
readonly config: Config
readonly database: Database
readonly database: TypeORMDatabase
readonly logger: Logger
readonly services: Services
readonly globalErrorHandler: ErrorRequestHandler

View File

@ -1,25 +1,25 @@
import { TypeORMDatabase } from '@rondo.dev/db-typeorm'
import { Routes } from '@rondo.dev/http-types'
import { bulkjsonrpc, jsonrpc } from '@rondo.dev/jsonrpc'
import { json } from 'body-parser'
import cookieParser from 'cookie-parser'
import { Database } from '../database'
import { loggerFactory } from '../logger'
import * as Middleware from '../middleware'
import { CSRFMiddleware, RequestLogger, TransactionMiddleware } from '../middleware'
import { TransactionalRouter } from '../router'
import * as routes from '../routes'
import { SQLTeamService, SQLUserService, Context } from '../rpc'
import { configureAuthRoutes } from '../routes/configureAuthRoutes'
import { Context, SQLTeamService, SQLUserService } 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 ServerConfig = ServerConfig
> = (
config: Config,
database: Database,
database: TypeORMDatabase,
) => T
export const configureServer: ServerConfigurator = (config, database) => {

View File

@ -1,15 +0,0 @@
import { Namespace } from 'cls-hooked'
import { Connection, EntityManager, EntitySchema, ObjectType, Repository } from 'typeorm'
import { TransactionManager } from './TransactionManager'
export interface Database {
namespace: Namespace
transactionManager: TransactionManager
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 TransactionManager {
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

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

View File

@ -5,7 +5,6 @@ if (require.main === module) {
}
export * from './application'
export * from './cli'
export * from './database'
export * from './entities'
export * from './error'
export * from './logger'

View File

@ -1,4 +1,3 @@
export * from './SQLLogger'
import loggerFactory from '@rondo.dev/logger'
export { loggerFactory }

View File

@ -1,15 +1,15 @@
import ExpressSession from 'express-session'
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'
import { TypeORMTransactionManager } from '@rondo.dev/db-typeorm'
export interface SessionMiddlewareParams {
transactionManager: TransactionManager
transactionManager: TypeORMTransactionManager
baseUrl: UrlWithStringQuery
sessionName: string
sessionSecret: string | string[]

View File

@ -1,16 +1,16 @@
import { TRANSACTION } from '@rondo.dev/db'
import { TypeORMDatabase, TypeORMLogger } from '@rondo.dev/db-typeorm'
import { createNamespace } from 'cls-hooked'
import express, { NextFunction, Request, Response } from 'express'
import request from 'supertest'
import { config } from '../config'
import { loggerFactory, SQLLogger } from '../logger'
import { loggerFactory } from '../logger'
import { CORRELATION_ID, TransactionMiddleware } from '../middleware'
import { ENTITY_MANAGER } from './'
import { SQLDatabase } from './SQLDatabase'
const ns = createNamespace('clsify-test')
const database = new SQLDatabase(
const database = new TypeORMDatabase(
ns,
new SQLLogger(loggerFactory.getLogger('sql'), ns),
new TypeORMLogger(loggerFactory.getLogger('sql'), ns),
config.app.db,
)
@ -65,7 +65,7 @@ describe('doInTransaction', () => {
app.use(new TransactionMiddleware(ns).handle)
app.use('/', (req, res, next) => {
if (entityManager) {
ns.set(ENTITY_MANAGER, entityManager)
ns.set(TRANSACTION, entityManager)
}
next()
})

View File

@ -1,6 +1,6 @@
import { TransactionManager } from '@rondo.dev/db'
import { Method, Routes } from '@rondo.dev/http-types'
import express from 'express'
import { TransactionManager } from '../database/TransactionManager'
import { AsyncRouter } from './AsyncRouter'
import { TypedHandler } from './TypedHandler'

View File

@ -1,15 +1,15 @@
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 { Database } from '../database/Database'
import { Team } from '../entities/Team'
import { UserTeam } from '../entities/UserTeam'
import { ensureLoggedIn, Context, RPC } from './RPC'
import { TypeORMDatabase } from '@rondo.dev/db-typeorm'
@ensureLoggedIn
export class SQLTeamService implements RPC<TeamService> {
constructor(
protected readonly db: Database,
protected readonly db: TypeORMDatabase,
protected readonly permissions: UserPermissions,
) {}

View File

@ -1,6 +1,6 @@
import { UserService } from '@rondo.dev/common'
import { TypeORMDatabase } from '@rondo.dev/db-typeorm'
import { hash } from 'bcrypt'
import { Database } from '../database/Database'
import { User } from '../entities/User'
import { UserEmail } from '../entities/UserEmail'
import { Context, ensureLoggedIn, RPC } from './RPC'
@ -10,7 +10,7 @@ const SALT_ROUNDS = 10
@ensureLoggedIn
export class SQLUserService implements RPC<UserService> {
constructor(protected readonly db: Database) {}
constructor(protected readonly db: TypeORMDatabase) {}
async getProfile(context: Context) {
const userId = context.user!.id

View File

@ -1,9 +1,9 @@
import { AuthService, Credentials, NewUser, UserProfile, trim } from '@rondo.dev/common'
import { AuthService, Credentials, NewUser, trim, UserProfile } from '@rondo.dev/common'
import { TypeORMDatabase } from '@rondo.dev/db-typeorm'
import Validator from '@rondo.dev/validator'
import { compare, hash } from 'bcrypt'
import { validate as validateEmail } from 'email-validator'
import createError from 'http-errors'
import { Database } from '../database/Database'
import { User } from '../entities/User'
import { UserEmail } from '../entities/UserEmail'
@ -11,7 +11,7 @@ const SALT_ROUNDS = 10
const MIN_PASSWORD_LENGTH = 10
export class SQLAuthService implements AuthService {
constructor(protected readonly db: Database) {}
constructor(protected readonly db: TypeORMDatabase) {}
async createUser(payload: NewUser): Promise<UserProfile> {
const newUser = {

View File

@ -1,10 +1,10 @@
import { UserPermissions } from '@rondo.dev/common'
import createError from 'http-errors'
import { Database } from '../database/Database'
import { UserTeam } from '../entities/UserTeam'
import { TypeORMDatabase } from '@rondo.dev/db-typeorm'
export class SQLUserPermissions implements UserPermissions {
constructor(protected readonly db: Database) {}
constructor(protected readonly db: TypeORMDatabase) {}
async belongsToTeam(params: {userId: number, teamId: number}) {
const {userId, teamId} = params

View File

@ -8,9 +8,10 @@ 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'
import { TypeORMTransactionManager } from '@rondo.dev/db-typeorm'
import { TRANSACTION_ID, TRANSACTION } from '@rondo.dev/db'
export class TestUtils<T extends Routes> {
readonly username = this.createTestUsername()
@ -18,7 +19,7 @@ export class TestUtils<T extends Routes> {
readonly app: AppServer
readonly context: string
readonly transactionManager: TransactionManager
readonly transactionManager: TypeORMTransactionManager
constructor(readonly bootstrap: Bootstrap) {
this.app = bootstrap.application.server
@ -54,7 +55,7 @@ export class TestUtils<T extends Routes> {
await queryRunner.connect()
namespace.set(TRANSACTION_ID, shortid())
await queryRunner.startTransaction()
namespace.set(ENTITY_MANAGER, queryRunner.manager)
namespace.set(TRANSACTION, queryRunner.manager)
})
afterEach(async () => {
@ -64,7 +65,7 @@ export class TestUtils<T extends Routes> {
}
await queryRunner.release()
namespace.set(TRANSACTION_ID, undefined)
namespace.set(ENTITY_MANAGER, undefined);
namespace.set(TRANSACTION, undefined);
(namespace as any).exit(context)
})

View File

@ -33,6 +33,12 @@
},
{
"path": "../validator/tsconfig.esm.json"
},
{
"path": "../db/tsconfig.esm.json"
},
{
"path": "../db-typeorm/tsconfig.esm.json"
}
]
}

View File

@ -14,6 +14,8 @@
{"path": "../tasq"},
{"path": "../http-client"},
{"path": "../http-types"},
{"path": "../validator"}
{"path": "../validator"},
{"path": "../db"},
{"path": "../db-typeorm"}
]
}