Last active
July 31, 2022 14:42
-
-
Save timwhit/3d44de267dd727b72780cbb348b315c3 to your computer and use it in GitHub Desktop.
TypeScript + Node.js Enterprise Patterns
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as express from 'express'; | |
import {injectable, inject} from 'inversify'; | |
import TYPES from '../types'; | |
import {AddressService} from '../service/AddressService'; | |
import {Address} from '../model/Address'; | |
import {RegistrableController} from './RegisterableController'; | |
@injectable() | |
export class AddressController implements RegistrableController { | |
private addressService: AddressService; | |
constructor(@inject(TYPES.AddressService) addressService: AddressService) { | |
this.addressService = addressService; | |
} | |
public register(app: express.Application): void { | |
app.route('/') | |
.get(async(req: express.Request, res: express.Response, next: express.NextFunction) => { | |
const addresses = await this.addressService.getAddresses().catch(err => next(err)); | |
res.json(addresses); | |
}) | |
.post(async(req: express.Request, res: express.Response, next: express.NextFunction) => { | |
const address = new Address( | |
req.body.address1, | |
req.body.address2, | |
req.body.city, | |
req.body.state, | |
req.body.zip, | |
req.body.country | |
); | |
const createdAddress = await this.addressService.createAddress(address).catch(err => next(err)); | |
res.json(createdAddress); | |
}); | |
app.route('/:id') | |
.get(async(req: express.Request, res: express.Response, next: express.NextFunction) => { | |
const addresses = await this.addressService.getAddress(<string> req.params.id).catch(err => next(err)); | |
res.json(addresses); | |
}) | |
.put(async(req: express.Request, res: express.Response, next: express.NextFunction) => { | |
const address = new Address( | |
req.body.address1, | |
req.body.address2, | |
req.body.city, | |
req.body.state, | |
req.body.zip, | |
req.body.country, | |
req.body.id | |
); | |
const updatedAddress = await this.addressService.updateAddress(address).catch(err => next(err)); | |
res.json(updatedAddress); | |
}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {injectable} from 'inversify'; | |
import {mongoDatabase, AddressDTO, AddressMongoSchema, AddressDbSchema} from '../model/AddressSchema'; | |
import {logger} from '../util/Logger'; | |
import {createConnection, Connection, Repository, ConnectionOptions} from 'typeorm'; | |
export interface AddressRepository { | |
findAll(): Promise<Array<AddressDTO>>; | |
create(addressDTO: AddressDTO): Promise<AddressDTO>; | |
update(addressDTO: AddressDTO): Promise<AddressDTO>; | |
find(id: string): Promise<AddressDTO>; | |
} | |
@injectable() | |
export class AddressRepositoryImplMongo implements AddressRepository { | |
public async findAll(): Promise<Array<AddressDTO>> { | |
const addressDTOs = await mongoDatabase.connect().then(() => mongoDatabase.Addresses.find()); | |
return addressDTOs.toArray(); | |
} | |
public async create(addressDTO: AddressDTO): Promise<AddressDTO> { | |
return await mongoDatabase.connect().then(() => mongoDatabase.Addresses.create(addressDTO)); | |
} | |
public async update(addressDTO: AddressDTO): Promise<AddressDTO> { | |
const dto: AddressMongoSchema = await mongoDatabase.connect().then(() => mongoDatabase.Addresses.findOne(addressDTO._id)); | |
dto.address1 = addressDTO.address1; | |
if (addressDTO.address2) { | |
dto.address2 = addressDTO.address2; | |
} else { | |
// undefined isn't handled by mongo, so set to null | |
dto.address2 = null; | |
} | |
dto.city = addressDTO.city; | |
dto.city = addressDTO.city; | |
dto.zip = addressDTO.zip; | |
dto.country = addressDTO.country; | |
const saved = await dto.save((err: Error, a: AddressDTO) => { | |
if (err) { | |
logger.error('Error updating address: ' + err); | |
throw err; | |
} | |
return a; | |
}); | |
return saved; | |
} | |
public async find(id: string): Promise<AddressDTO> { | |
return await mongoDatabase.connect().then(() => mongoDatabase.Addresses.findOne(id)); | |
} | |
} | |
@injectable() | |
export class AddressRepositoryImplDb implements AddressRepository { | |
private addressRepository: Repository<AddressDbSchema>; | |
constructor() { | |
this.connect().then(async connection => { | |
this.addressRepository = connection.getRepository(AddressDbSchema); | |
}).catch(err => logger.error('Cannot connect to database', err)); | |
} | |
public async findAll(): Promise<Array<AddressDTO>> { | |
return await this.addressRepository.find(); | |
} | |
public async create(addressDTO: AddressDTO): Promise<AddressDTO> { | |
return await this.addressRepository.persist(addressDTO); | |
} | |
public async update(addressDTO: AddressDTO): Promise<AddressDTO> { | |
return await this.addressRepository.persist(addressDTO); | |
} | |
public async find(id: string): Promise<AddressDTO> { | |
return await this.addressRepository.findOneById(id); | |
} | |
private connect(): Promise<Connection> { | |
return createConnection(<ConnectionOptions> { | |
driver: { | |
type: 'sqlite', | |
storage: 'tmp/sqlitedb.db' | |
}, | |
logging: { | |
logQueries: true, | |
logSchemaCreation: true | |
}, | |
autoSchemaSync: true, | |
entities: [ | |
AddressDbSchema | |
] | |
}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {Core, Model, Instance, Collection, Index, Property, ObjectID} from 'iridium'; | |
import {Table, Column, PrimaryColumn} from 'typeorm'; | |
export interface AddressDTO { | |
_id?: string; | |
address1: string; | |
address2?: string; | |
city: string; | |
state: string; | |
zip: string; | |
country: string; | |
} | |
/** | |
* Iridium config | |
*/ | |
@Index({name: 1}) | |
@Collection('addresses') | |
export class AddressMongoSchema extends Instance<AddressDTO, AddressMongoSchema> implements AddressDTO { | |
@ObjectID | |
// tslint:disable-next-line:variable-name | |
public _id: string; | |
@Property(String, true) | |
public address1: string; | |
@Property(String, false) | |
public address2: string; | |
@Property(String, true) | |
public city: string; | |
@Property(String, true) | |
public state: string; | |
@Property(String, true) | |
public zip: string; | |
@Property(String, true) | |
public country: string; | |
} | |
class AddressDatabase extends Core { | |
public Addresses = new Model<AddressDTO, AddressMongoSchema>(this, AddressMongoSchema); | |
} | |
export const mongoDatabase = new AddressDatabase({database: 'test_db'}); | |
/** | |
* TypeORM Schema Config | |
*/ | |
@Table('address') | |
export class AddressDbSchema implements AddressDTO { | |
@PrimaryColumn() | |
// tslint:disable-next-line:variable-name | |
public _id?: string; | |
@Column() | |
public address1: string; | |
@Column() | |
public address2?: string; | |
@Column() | |
public city: string; | |
@Column() | |
public state: string; | |
@Column() | |
public zip: string; | |
@Column() | |
public country: string; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {injectable, inject} from 'inversify'; | |
import {Address} from '../model/Address'; | |
import {AddressRepository} from '../repository/AddressRepository'; | |
import TYPES from '../types'; | |
import 'reflect-metadata'; | |
import {AddressDTO} from '../model/AddressSchema'; | |
import * as _ from 'lodash'; | |
export interface AddressService { | |
getAddresses(): Promise<Array<Address>>; | |
createAddress(address: Address): Promise<Address>; | |
updateAddress(address: Address): Promise<Address>; | |
getAddress(id: string): Promise<Address>; | |
} | |
@injectable() | |
export class AddressServiceImpl implements AddressService { | |
@inject(TYPES.AddressRepository) | |
private addressRepositoryMongo: AddressRepository; | |
@inject(TYPES.AddressRepository2) | |
private addressRepositoryDb: AddressRepository; | |
public async getAddresses(): Promise<Array<Address>> { | |
// grab addresses from mongo | |
const addressesMongo: Array<Address> = await this.addressRepositoryMongo.findAll().then((a) => a.map((dto: AddressDTO) => { | |
return this.toAddressDTO(dto); | |
})); | |
// grab addresses from db | |
const addressesDb: Array<Address> = await this.addressRepositoryDb.findAll().then((a2) => a2.map((dto: AddressDTO) => { | |
return this.toAddressDTO(dto); | |
})); | |
return _.uniqBy(addressesMongo.concat(addressesDb), 'id'); | |
} | |
public async createAddress(address: Address): Promise<Address> { | |
const addressDTO: AddressDTO = this.toAddress(address); | |
const createdDTO: AddressDTO = await this.addressRepositoryMongo.create(addressDTO); | |
// duplicates the address in the DB | |
await this.addressRepositoryDb.create(await createdDTO); | |
return await this.toAddressDTO(createdDTO); | |
} | |
public async updateAddress(address: Address): Promise<Address> { | |
const addressDTO: AddressDTO = this.toAddress(address); | |
const updated: AddressDTO = await this.addressRepositoryMongo.update(addressDTO); | |
// update db address | |
await this.addressRepositoryDb.update(updated); | |
return await this.toAddressDTO(updated); | |
} | |
public async getAddress(id: string): Promise<Address> { | |
let address = await this.addressRepositoryMongo.find(id).then((a) => { | |
return this.toAddressDTO(a); | |
}); | |
if (!address) { | |
address = await this.addressRepositoryDb.find(id).then((a) => { | |
return this.toAddressDTO(a); | |
}); | |
} | |
return address; | |
} | |
private toAddress(address: Address): AddressDTO { | |
return { | |
address1: address.getAddress1, | |
address2: address.getAddress2, | |
city: address.getCity, | |
state: address.getState, | |
zip: address.getZip, | |
country: address.getCountry, | |
_id: address.getId | |
}; | |
} | |
private toAddressDTO(addressDTO: AddressDTO): Address { | |
return new Address( | |
addressDTO.address1, | |
addressDTO.address2, | |
addressDTO.city, | |
addressDTO.state, | |
addressDTO.zip, | |
addressDTO.country, | |
addressDTO._id.toString()); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as express from 'express'; | |
import * as bodyParser from 'body-parser'; | |
import TYPES from './types'; | |
import container from './inversify.config'; | |
import {logger} from './util/Logger'; | |
import {RegistrableController} from './controller/RegisterableController'; | |
// create express application | |
const app: express.Application = express(); | |
// let express support JSON bodies | |
app.use(bodyParser.json()); | |
// grabs the Controller from IoC container and registers all the endpoints | |
const controllers: RegistrableController[] = container.getAll<RegistrableController>(TYPES.Controller); | |
controllers.forEach(controller => controller.register(app)); | |
// setup express middleware logging and error handling | |
app.use(function (err: Error, req: express.Request, res: express.Response, next: express.NextFunction) { | |
logger.error(err.stack); | |
next(err); | |
}); | |
app.use(function (err: Error, req: express.Request, res: express.Response, next: express.NextFunction) { | |
res.status(500).send('Internal Server Error'); | |
}); | |
app.listen(3000, function () { | |
logger.info('Example app listening on port 3000!'); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {Container} from 'inversify'; | |
import TYPES from './types'; | |
import {AddressService, AddressServiceImpl} from './service/AddressService'; | |
import {AddressRepository, AddressRepositoryImplMongo, AddressRepositoryImplDb} from './repository/AddressRepository'; | |
import {AddressController} from './controller/AddressController'; | |
import {RegistrableController} from './controller/RegisterableController'; | |
const container = new Container(); | |
container.bind<RegistrableController>(TYPES.Controller).to(AddressController); | |
container.bind<AddressService>(TYPES.AddressService).to(AddressServiceImpl); | |
container.bind<AddressRepository>(TYPES.AddressRepository).to(AddressRepositoryImplMongo); | |
container.bind<AddressRepository>(TYPES.AddressRepository2).to(AddressRepositoryImplDb); | |
export default container; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as express from 'express'; | |
export interface RegistrableController { | |
register(app: express.Application): void; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const TYPES = { | |
AddressRepository: Symbol('AddressRepository'), | |
AddressRepository2: Symbol('AddressRepository2'), | |
AddressService: Symbol('AddressService'), | |
Controller: Symbol('Controller') | |
}; | |
export default TYPES; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment