@athenna/database
Version:
The Athenna database handler for SQL/NoSQL.
426 lines (425 loc) • 18.5 kB
JavaScript
/**
* @athenna/database
*
* (c) João Lenon <lenon@athenna.io>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import { sep } from 'node:path';
import { Json, Path, File, String } from '@athenna/common';
import { BaseCommand, Option, Argument, Generator } from '@athenna/artisan';
export class MakeCrudCommand extends BaseCommand {
constructor() {
super(...arguments);
this.properties = [];
}
static signature() {
return 'make:crud';
}
static description() {
return 'Make a new CRUD in your application.';
}
cleanGenerator() {
this.generator = new Generator();
}
toCase(value) {
const fileCase = Config.get('rc.commands.make:crud.fileCase', 'toPascalCase');
return String[fileCase](value);
}
async handle() {
this.logger.simple('({bold,green} [ MAKING CRUD ])\n');
this.namePascal = String.toPascalCase(this.name);
this.nameLower = this.name.toLowerCase();
const addId = await this.prompt.confirm(`Do you want to add the ${this.paint.yellow('"id"')} property?`);
if (addId) {
this.properties.push({ name: 'id', type: 'increments', custom: false });
}
const addTimestamps = await this.prompt.confirm(`Does your CRUD need ${this.paint.yellow('"createdAt/updateAt"')} properties?`);
const addSoftDelete = await this.prompt.confirm(`Does your CRUD needs ${this.paint.yellow('"deletedAt"')} property? (Soft Delete)`);
let addMoreProps = await this.prompt.confirm('Do you want to add more properties to your CRUD?');
while (addMoreProps) {
const name = await this.prompt.input('What will be the name of your property?');
const type = await this.prompt.list('What will be the type of your property?', ['string', 'number', 'boolean', 'Date']);
const options = await this.prompt.checkbox('Select the options that fits your property:', [
'isPrimary',
'isUnique',
'isHidden',
'notNullable',
'isIndex',
'isSparse'
]);
const optionsObj = {};
options.forEach(option => (optionsObj[option] = true));
this.properties.push({ name, type, custom: true, ...optionsObj });
addMoreProps = await this.prompt.confirm('Do you want to add more properties?');
}
if (addTimestamps) {
this.properties.push({
name: 'createdAt',
type: 'Date',
custom: false,
isCreateDate: true
});
this.properties.push({
name: 'updatedAt',
type: 'Date',
custom: false,
isUpdateDate: true
});
}
if (addSoftDelete) {
this.properties.push({
name: 'deletedAt',
type: 'Date',
custom: false,
isDeleteDate: true
});
}
console.log();
const task = this.logger.task();
if (Config.get('rc.commands.make:crud.model.enabled', true)) {
task.addPromise('Creating model', () => this.makeModel());
}
if (!this.isMongo &&
Config.get('rc.commands.make:crud.migration.enabled', true)) {
task.addPromise('Creating migration', () => this.makeMigration());
}
if (Config.get('rc.commands.make:crud.controller.enabled', true)) {
task.addPromise('Creating controller', () => this.makeController());
task.addPromise('Adding CRUD routes', () => this.addRoutes());
}
if (Config.get('rc.commands.make:crud.service.enabled', true)) {
task.addPromise('Creating service', () => this.makeService());
}
if (Config.get('rc.commands.make:crud.controller-test.enabled', true)) {
task.addPromise('Creating e2e tests for controller', () => this.makeControllerTest());
}
if (Config.get('rc.commands.make:crud.service-test.enabled', true)) {
task.addPromise('Creating unitary tests for service', () => this.makeServiceTest());
}
await task.run();
console.log();
this.logger.success(`CRUD ({yellow} "${this.name}") successfully created.`);
}
async makeModel() {
this.cleanGenerator();
const destination = Config.get('rc.commands.make:crud.model.destination', Path.models());
let properties = '';
let definitions = '';
this.properties.forEach((p, i) => {
const property = Json.copy(p);
let annotationProps = '';
if (property.isPrimary) {
annotationProps += 'isPrimary: true, ';
}
if (property.isUnique) {
annotationProps += 'isUnique: true, ';
}
if (property.isHidden) {
annotationProps += 'isHidden: true, ';
}
if (property.notNullable) {
annotationProps += 'isNullable: false, ';
}
if (property.isIndex) {
annotationProps += 'isIndex: true, ';
}
if (property.isSparse) {
annotationProps += 'isSparse: true, ';
}
if (property.isCreateDate) {
annotationProps += 'isCreateDate: true, ';
}
if (property.isUpdateDate) {
annotationProps += 'isUpdateDate: true, ';
}
if (property.isDeleteDate) {
annotationProps += 'isDeleteDate: true, ';
}
if (property.type === 'increments') {
property.type = this.isMongo ? 'string' : 'number';
}
if (annotationProps.length) {
properties += ` \n public ${property.name}: ${property.type}`;
}
else {
properties += ` \n public ${property.name}: ${property.type}`;
}
const type = {
string: 'this.faker.string.sample()',
number: 'this.faker.number.int({ max: 10000000 })',
boolean: 'this.faker.datatype.boolean()',
Date: 'this.faker.date.anytime()'
};
if (property.custom) {
definitions += ` ${property.name}: ${type[property.type]}`;
}
if (this.properties.length - 1 !== i) {
properties += '\n\n';
if (definitions.length && property.custom)
definitions += ',\n';
}
});
await this.generator
.fileName(this.toCase(this.name))
.destination(destination)
.template('crud-model')
.properties({ properties, definitions })
.setNameProperties(true)
.make();
const importPath = this.generator.getImportPath();
await this.rc.pushTo('models', importPath).save();
}
async makeMigration() {
this.cleanGenerator();
const destination = Config.get('rc.commands.make:crud.migration.destination', Path.migrations());
let properties = '';
this.properties.forEach((property, i) => {
if (property.isCreateDate || property.isUpdateDate) {
if (properties.includes('builder.timestamps(true, true, true)')) {
return;
}
properties += ` builder.timestamps(true, true, true)${this.properties.length - 1 !== i ? '\n' : ''}`;
return;
}
switch (property.type) {
case 'increments':
properties += ` builder.increments('${property.name}')`;
break;
case 'string':
properties += ` builder.string('${property.name}')`;
break;
case 'boolean':
properties += ` builder.boolean('${property.name}')`;
break;
case 'number':
properties += ` builder.integer('${property.name}')`;
break;
case 'Date':
properties += ` builder.timestamp('${property.name}')`;
}
if (property.isPrimary) {
properties += '.primary()';
}
if (property.isUnique) {
properties += '.unique()';
}
if (!property.notNullable && property.type !== 'increments') {
properties += '.nullable()';
}
if (property.isIndex) {
properties += '.index()';
}
if (this.properties.length - 1 !== i) {
properties += '\n';
}
});
const tableName = String.pluralize(this.name);
const namePascal = String.toPascalCase(`Create_${tableName}_Table`);
let [date, time] = new Date().toISOString().split('T');
date = date.replace(/-/g, '_');
time = time.split('.')[0].replace(/:/g, '');
await this.generator
.fileName(`${sep}${date}_${time}_create_${tableName}_table`)
.destination(destination)
.properties({ namePascal, properties, tableName })
.template('crud-migration')
.setNameProperties(false)
.make();
}
async makeController() {
this.cleanGenerator();
const destination = Config.get('rc.commands.make:crud.controller.destination', Path.controllers());
const serviceDest = Config.get('rc.commands.make:crud.service.destination', Path.services());
let properties = '';
this.properties
.filter(p => p.custom)
.forEach(p => {
properties += `'${p.name}', `;
});
this.generator
.fileName(this.toCase(`${this.name}Controller`))
.destination(destination)
.properties({
properties: properties.slice(0, properties.length - 2),
serviceImportPath: new Generator()
.fileName(this.toCase(`${this.name}Service`))
.destination(serviceDest)
.getImportPath(),
crudNamePascal: this.namePascal,
crudNameLower: this.nameLower
})
.template('crud-controller')
.setNameProperties(true);
await this.generator.make();
const importPath = this.generator.getImportPath();
await this.rc.pushTo('controllers', importPath).save();
}
async addRoutes() {
const routeFilePath = Config.get('rc.commands.make:crud.routeFilePath', Path.routes(`http.${Path.ext()}`));
const path = `/${String.pluralize(this.nameLower)}`;
const controller = `${this.namePascal}Controller`;
let body = '';
this.properties
.filter(p => p.custom)
.forEach(property => {
const type = {
string: 'string',
number: 'number',
boolean: 'boolean',
Date: 'string'
};
body += `.body('${property.name}', { type: '${type[property.type]}' })`;
});
const routeContent = `
Route.get('${path}', '${controller}.index')
Route.post('${path}', '${controller}.store')${body}
Route.get('${path}/:id', '${controller}.show')
Route.put('${path}/:id', '${controller}.update')${body}
Route.delete('${path}/:id', '${controller}.delete')
\n`;
await new File(routeFilePath, `import { Route } from '@athenna/http'\n\n`).append(routeContent);
}
async makeService() {
this.cleanGenerator();
const destination = Config.get('rc.commands.make:crud.service.destination', Path.services());
const modelDest = Config.get('rc.commands.make:crud.model.destination', Path.models());
let propertiesToUpdate = '';
this.properties
.filter(p => p.custom)
.forEach(property => {
propertiesToUpdate += ` ${this.nameLower}.${property.name} = body.${property.name}\n`;
});
await this.generator
.fileName(this.toCase(`${this.name}Service`))
.destination(destination)
.properties({
propertiesToUpdate,
idType: this.isMongo ? 'string' : 'number',
modelImportPath: new Generator()
.fileName(this.toCase(this.name))
.destination(modelDest)
.getImportPath(),
crudNamePascal: this.namePascal,
crudNameLower: this.nameLower
})
.template('crud-service')
.setNameProperties(true)
.make();
const importPath = this.generator.getImportPath();
await this.rc.pushTo('services', importPath).save();
}
async makeControllerTest() {
this.cleanGenerator();
const destination = Config.get('rc.commands.make:crud.controller-test.destination', Path.tests('e2e'));
const modelDest = Config.get('rc.commands.make:crud.model.destination', Path.models());
let createBody = '';
let updateBody = '';
let showAssertBody = `id: ${this.nameLower}.id, `;
let createAssertBody = '';
let updateAssertBody = `id: ${this.nameLower}.id, `;
this.properties
.filter(p => p.custom)
.forEach(property => {
const type = {
string: `'string'`,
number: 1,
boolean: true,
Date: 'new Date()'
};
createBody += `${property.name}: ${type[property.type]}, `;
updateBody += `${property.name}: ${type[property.type]}, `;
showAssertBody += `${property.name}: ${type[property.type]}, `;
createAssertBody += `'${property.name}', `;
updateAssertBody += `${property.name}: ${type[property.type]}, `;
});
await this.generator
.fileName(this.toCase(`${this.name}ControllerTest`))
.destination(destination)
.properties({
hasDeletedAt: this.properties.find(p => p.name === 'deletedAt'),
createBody: createBody.slice(0, createBody.length - 2),
updateBody: updateBody.slice(0, updateBody.length - 2),
showAssertBody: showAssertBody.slice(0, showAssertBody.length - 2),
createAssertBody: createAssertBody.slice(0, createAssertBody.length - 2),
updateAssertBody: updateAssertBody.slice(0, updateAssertBody.length - 2),
modelImportPath: new Generator()
.fileName(this.toCase(this.name))
.destination(modelDest)
.getImportPath(),
crudNamePascal: this.namePascal,
crudNamePascalPlural: String.pluralize(this.namePascal),
crudNameLower: this.nameLower,
crudNameLowerPlural: String.pluralize(this.nameLower)
})
.template('crud-controller-test')
.setNameProperties(true)
.make();
}
async makeServiceTest() {
this.cleanGenerator();
const destination = Config.get('rc.commands.make:crud.service-test.destination', Path.tests('unit'));
const modelDest = Config.get('rc.commands.make:crud.model.destination', Path.models());
const serviceDest = Config.get('rc.commands.make:crud.service.destination', Path.services());
let createBody = '';
let updateBody = '';
this.properties
.filter(p => p.custom)
.forEach(property => {
const type = {
string: `'string'`,
number: 1,
boolean: true,
Date: 'new Date()'
};
createBody += `${property.name}: ${type[property.type]}, `;
updateBody += `${property.name}: ${type[property.type]}, `;
});
await this.generator
.fileName(this.toCase(`${this.name}ServiceTest`))
.destination(destination)
.properties({
createBody: createBody.slice(0, createBody.length - 2),
updateBody: updateBody.slice(0, updateBody.length - 2),
modelImportPath: new Generator()
.fileName(this.toCase(this.name))
.destination(modelDest)
.getImportPath(),
serviceImportPath: new Generator()
.fileName(this.toCase(`${this.name}Service`))
.destination(serviceDest)
.getImportPath(),
crudNamePascal: this.namePascal,
crudNamePascalPlural: String.pluralize(this.namePascal),
crudNameLower: this.nameLower,
crudNameLowerPlural: String.pluralize(this.nameLower)
})
.template('crud-service-test')
.setNameProperties(true)
.make();
}
}
__decorate([
Argument({
description: 'The crud name.'
}),
__metadata("design:type", String)
], MakeCrudCommand.prototype, "name", void 0);
__decorate([
Option({
default: false,
signature: '--mongo',
description: 'Define if CRUD will use Mongo as database. Migration will be skipped and "id" will be defined as string.'
}),
__metadata("design:type", Boolean)
], MakeCrudCommand.prototype, "isMongo", void 0);