express-typeorm-rest-boilerplate
Version:
Boilerplate code to get started with building RESTful API Services
460 lines (440 loc) • 15.8 kB
text/typescript
// eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require('fs').promises;
import pluralize from 'pluralize';
const camelCase = (str: string): string =>
str.charAt(0).toLowerCase() + str.slice(1);
const pluralizeLastWord = (str: string): string => {
let foundIndex = 0;
for (let i = str.length - 1; i >= 0; i--) {
if (str.charAt(i) == str.charAt(i).toUpperCase()) {
foundIndex = i;
break;
}
}
const pluralWord = pluralize(str.substring(foundIndex));
return str.substring(0, foundIndex) + pluralWord;
};
export default class Generator {
private entityName: string;
private fields: unknown;
private entityPath = 'src/api/entities/';
private servicePath = 'src/api/services/';
private routePath = 'src/api/routes/';
private factoryPath = 'src/database/factories/';
private testPath = 'test/integration/';
constructor(
entityName: string,
entityPath?: string,
servicePath?: string,
routePath?: string,
factoryPath?: string,
testPath?: string
) {
this.entityName = entityName;
this.entityPath = this.entityPath || entityPath;
this.servicePath = this.servicePath || servicePath;
this.routePath = this.routePath || routePath;
this.factoryPath = this.factoryPath || factoryPath;
this.testPath = this.testPath || testPath;
}
public readEntityFields = async (): Promise<unknown> => {
const fields = {};
try {
const entitySource: string = await fs.readFile(
this.entityPath + this.entityName + '.ts',
'utf8'
);
const lines = entitySource.split('\n');
let obj = null;
for (const line of lines) {
const ind: number = line.indexOf('?:');
if (
ind !== -1 &&
line.indexOf(' id?:') === -1 &&
line.indexOf('constructor') === -1
) {
const fieldName = line.substring(0, ind).trim();
if (line.indexOf('}') !== -1) obj = null;
if (line.indexOf('{') === -1) {
if (obj) {
fields[obj][fieldName] = '';
} else {
fields[fieldName] = '';
}
} else {
fields[fieldName] = {};
obj = fieldName;
}
}
}
} catch (err) {
console.log(err);
return null;
}
this.fields = fields;
return fields;
};
private entityCode = (fields: string): string => {
let res =
"import { Entity, ObjectIdColumn, Column, ObjectID } from 'typeorm';\n" +
"import { IsString } from 'class-validator';\n" +
'\n' +
'@Entity()\n' +
'export class ' +
this.entityName +
' {\n' +
' @ObjectIdColumn()\n' +
' id?: ObjectID;\n';
if (fields) {
const fieldsArr = fields.split(' ');
for (const field of fieldsArr) {
res +=
'\n' +
' @Column()\n' +
' @IsString()\n' +
` ${field.trim()}?: string;\n`;
}
res +=
'\n' +
` public constructor(data?: ${this.entityName}) {\n` +
' if (data) {\n';
for (const field of fieldsArr) {
const trimmedField = field.trim();
res += ` this.${trimmedField} = data.${trimmedField};\n`;
}
res += ' }\n' + ' }\n' + ' }\n';
} else {
res +=
'\n' +
' @Column()\n' +
' @IsString()\n' +
' field?: string;\n' +
'\n' +
` public constructor(data?: ${this.entityName}) {\n` +
' if (data) {\n' +
' this.field = data.field;\n' +
' }\n' +
' }\n' +
' }\n';
}
res += '}\n';
return res;
};
private serviceCode = (): string => {
return (
"import { Inject, Service } from 'typedi';\n" +
`import { ${this.entityName} } from '../entities/${this.entityName}';\n` +
"import { MongoRepository } from 'typeorm';\n" +
"import { InjectRepository } from 'typeorm-typedi-extensions';\n" +
"import { Logger } from 'winston';\n" +
"import CRUD from './CRUD';\n" +
'\n' +
'@Service()\n' +
`export default class ${this.entityName}Service extends CRUD<${this.entityName}> {\n` +
' constructor(\n' +
` @InjectRepository(${this.entityName})\n` +
` protected repo: MongoRepository<${this.entityName}>,\n` +
" @Inject('logger')\n" +
' protected logger: Logger\n' +
' ) {\n' +
' super(repo, logger);\n' +
' }\n' +
'\n' +
` async create(${camelCase(this.entityName)}: ${
this.entityName
}): Promise<${this.entityName}> {\n` +
` return await super.create(${camelCase(
this.entityName
)}, 'uniqueFieldName');\n` +
' }\n' +
'}\n'
);
};
private routeFunctionHeader = (
method: string,
routePath: string,
validation?: string
): string => {
if (validation) {
return (
`route.${method}(\n` +
` '${routePath}',\n` +
' isAuth,\n' +
' celebrate({\n' +
' body: Joi.object({\n' +
validation +
' }),\n' +
' }),\n' +
' async (req, res, next) => {\n'
);
}
return `route.${method}('${routePath}', isAuth, async (req, res, next) => {\n`;
};
private routeValidation = (fields: unknown, required?: boolean): string => {
let res = '';
for (const [key, value] of Object.entries(fields)) {
if (typeof value === 'object' && value !== null) {
res += ` ${key}: Joi.object({\n`;
for (const [_key, _value] of Object.entries(value)) {
res += ` ${_key}: Joi.string()${
required ? '.required()' : ''
},\n`;
}
res += ` })${required ? '.required()' : ''},\n`;
} else {
res += ` ${key}: Joi.string()${required ? '.required()' : ''},\n`;
}
}
return res;
};
private routeCode = (fields: unknown): string => {
return (
"import { Router } from 'express';\n" +
"import { Container } from 'typedi';\n" +
"import { celebrate, Joi } from 'celebrate';\n" +
`import ${this.entityName}Service from '../services/${this.entityName}Service';\n` +
"import { Logger } from 'winston';\n" +
`import { ${this.entityName} } from '../entities/${this.entityName}';\n` +
"import { isAuth } from '../middlewares';\n" +
'\n' +
'const route = Router();\n' +
'\n' +
this.routeFunctionHeader('get', '/') +
" const logger: Logger = Container.get('logger');\n" +
` logger.debug('Calling GET to /${camelCase(
this.entityName
)} endpoint');\n` +
' try {\n' +
` const ${camelCase(this.entityName)}ServiceInstance = Container.get(${
this.entityName
}Service);\n` +
` const ${camelCase(
pluralizeLastWord(this.entityName)
)} = await ${camelCase(this.entityName)}ServiceInstance.find();\n` +
` return res.json(${camelCase(
pluralizeLastWord(this.entityName)
)}).status(200);\n` +
' } catch (e) {\n' +
' return next(e);\n' +
' }\n' +
'});\n' +
'\n' +
this.routeFunctionHeader('get', '/:id') +
" const logger: Logger = Container.get('logger');\n" +
` logger.debug('Calling GET to /${camelCase(
this.entityName
)}/:id endpoint with id: %s', req.params.id);\n` +
' try {\n' +
` const ${camelCase(this.entityName)}ServiceInstance = Container.get(${
this.entityName
}Service);\n` +
` const ${camelCase(this.entityName)} = await ${camelCase(
this.entityName
)}ServiceInstance.findOne(req.params.id);\n` +
` return res.json(${camelCase(this.entityName)}).status(200);\n` +
' } catch (e) {\n' +
' return next(e);\n' +
' }\n' +
'});\n' +
'\n' +
this.routeFunctionHeader('delete', '/:id') +
" const logger: Logger = Container.get('logger');\n" +
` logger.debug('Calling DELETE to /${camelCase(
this.entityName
)}/:id endpoint with id: %s', req.params.id);\n` +
' try {\n' +
` const ${camelCase(this.entityName)}ServiceInstance = Container.get(${
this.entityName
}Service);\n` +
` await ${camelCase(
this.entityName
)}ServiceInstance.delete(req.params.id);\n` +
` return res.status(204).end();\n` +
' } catch (e) {\n' +
' return next(e);\n' +
' }\n' +
'});\n' +
'\n' +
this.routeFunctionHeader(
'post',
'/',
this.routeValidation(fields, true)
) +
" const logger: Logger = Container.get('logger');\n" +
` logger.debug('Calling POST to /${camelCase(
this.entityName
)}/:id endpoint with body: %o', req.body);\n` +
' try {\n' +
` const ${camelCase(
this.entityName
)}ServiceInstance = Container.get(${this.entityName}Service);\n` +
` const ${camelCase(this.entityName)} = await ${camelCase(
this.entityName
)}ServiceInstance.create(\n` +
` new ${this.entityName}(req.body)\n` +
' );\n' +
` return res.json(${camelCase(this.entityName)}).status(201);\n` +
' } catch (e) {\n' +
' return next(e);\n' +
' }\n' +
' });\n' +
'\n' +
this.routeFunctionHeader('put', '/:id', this.routeValidation(fields)) +
" const logger: Logger = Container.get('logger');\n" +
` logger.debug('Calling PUT to /${camelCase(
this.entityName
)}/:id endpoint with body: %o', req.body);\n` +
' try {\n' +
` const ${camelCase(
this.entityName
)}ServiceInstance = Container.get(${this.entityName}Service);\n` +
` const ${camelCase(this.entityName)} = await ${camelCase(
this.entityName
)}ServiceInstance.update(\n` +
` req.params.id,\n` +
` new ${this.entityName}(req.body)\n` +
' );\n' +
` return res.json(${camelCase(this.entityName)}).status(200);\n` +
' } catch (e) {\n' +
' return next(e);\n' +
' }\n' +
' }\n' +
');\n' +
'\n' +
'export default route;\n'
);
};
private factoryCode = (fields: unknown): string => {
let res =
`import { ${this.entityName} } from '../../api/entities/${this.entityName}';\n` +
"import * as faker from 'faker';\n" +
'\n' +
`export default (data?: ${this.entityName}): ${this.entityName} => {\n` +
` const ${camelCase(this.entityName)} = new ${this.entityName}({\n`;
for (const [key, value] of Object.entries(fields)) {
if (typeof value === 'object' && value !== null) {
res += ` ${key}: (data && data.${key}) || {\n`;
for (const [_key, _value] of Object.entries(value)) {
res += ` ${_key}: faker.random.word(),\n`;
}
res += ' },\n';
} else {
res += ` ${key}: (data && data.${key}) || faker.random.word(),\n`;
}
}
res += ' });\n' + ` return ${camelCase(this.entityName)};\n` + '};\n';
return res;
};
private testCode = (): string =>
"import { Container } from 'typedi';\n" +
`import ${this.entityName}Service from '../../src/api/services/${this.entityName}Service';\n` +
"import databaseLoader from '../../src/loaders/database';\n" +
"import { Connection } from 'typeorm';\n" +
"import Logger from '../../src/logger';\n" +
`import ${this.entityName}Factory from '../../src/database/factories/${this.entityName}Factory';\n` +
`import { ${this.entityName} } from '../../src/api/entities/${this.entityName}';\n` +
"import EntitySeed from '../../src/database/seeds/EntitySeed';\n" +
"import { ErrorHandler } from '../../src/helpers/ErrorHandler';\n" +
"jest.mock('../../src/logger');\n" +
'\n' +
`describe('${this.entityName}Service', () => {\n` +
' let connection: Connection;\n' +
` let ${camelCase(this.entityName)}Seed: EntitySeed<${
this.entityName
}>;\n` +
` let ${camelCase(this.entityName)}ServiceInstance: ${
this.entityName
}Service;\n` +
' beforeAll(async (done) => {\n' +
' Container.reset();\n' +
' connection = await databaseLoader();\n' +
' await connection.synchronize(true);\n' +
` ${camelCase(this.entityName)}Seed = new EntitySeed<${
this.entityName
}>(\n` +
` connection.getMongoRepository(${this.entityName}),\n` +
` ${this.entityName}Factory\n` +
' );\n' +
" Container.set('logger', Logger);\n" +
` ${camelCase(this.entityName)}ServiceInstance = Container.get(${
this.entityName
}Service);\n` +
' done();\n' +
' });\n' +
'\n' +
' beforeEach(async (done) => {\n' +
' await connection.dropDatabase();\n' +
' done();\n' +
' });\n' +
'\n' +
' afterAll(async (done) => {\n' +
' if (connection.isConnected) {\n' +
' await connection.close();\n' +
' }\n' +
' done();\n' +
' });\n' +
'\n' +
" describe('create', () => {\n" +
` test('Should successfully create a ${camelCase(
this.entityName
)} record', async () => {\n` +
` const mock${this.entityName} = ${this.entityName}Factory();\n` +
` const response = await ${camelCase(
this.entityName
)}ServiceInstance.create(mock${this.entityName});\n` +
'\n' +
' expect(response).toBeDefined();\n' +
' expect(response.id).toBeDefined();\n' +
' });\n' +
'\n' +
` test('Should fail to create a ${camelCase(
this.entityName
)} record if the ${camelCase(
this.entityName
)} already exists', async () => {\n` +
` const existing${this.entityName} = await ${camelCase(
this.entityName
)}Seed.seedOne();\n` +
` let err: ErrorHandler, response: ${this.entityName};\n` +
' try {\n' +
` response = await ${camelCase(
this.entityName
)}ServiceInstance.create(existing${this.entityName});\n` +
' } catch (e) {\n' +
' err = e;\n' +
' }\n' +
' expect(response).toBeUndefined();\n' +
` expect(err).toEqual(new ErrorHandler(400, 'The ${this.entityName} already exists'));\n` +
' });\n' +
' });\n' +
'});\n';
public generateEntity = async (fields: string): Promise<string> => {
const filePath: string = this.entityPath + this.entityName + '.ts';
await fs.writeFile(filePath, this.entityCode(fields));
return filePath;
};
public generateService = async (): Promise<string> => {
const filePath: string = this.servicePath + this.entityName + 'Service.ts';
await fs.writeFile(filePath, this.serviceCode());
return filePath;
};
public generateRoute = async (): Promise<string> => {
const fields = this.fields || (await this.readEntityFields());
const filePath: string =
this.routePath + camelCase(this.entityName) + '.ts';
await fs.writeFile(filePath, this.routeCode(fields));
return filePath;
};
public generateFactory = async (): Promise<string> => {
const fields = this.fields || (await this.readEntityFields());
const filePath: string = this.factoryPath + this.entityName + 'Factory.ts';
await fs.writeFile(filePath, this.factoryCode(fields));
return filePath;
};
public generateTest = async (): Promise<string> => {
const filePath: string =
this.testPath + camelCase(this.entityName) + 'Service.spec.ts';
await fs.writeFile(filePath, this.testCode());
return filePath;
};
}