@codehance/rapid-stack
Version:
A modern full-stack development toolkit for rapid application development
268 lines (229 loc) • 8.49 kB
JavaScript
const Generator = require('yeoman-generator');
const path = require('path');
const fs = require('fs');
const _ = require('lodash');
module.exports = class extends Generator {
constructor(args, opts) {
super(args, opts);
// Add frontend paths
this.frontendPath = './frontend';
this.graphqlPath = 'frontend/src/app/graphql';
}
// Helper functions
_capitalize(str) {
return _.upperFirst(_.camelCase(str));
}
_pluralize(str) {
// Basic pluralization rules
if (str.endsWith('y')) {
return str.slice(0, -1) + 'ies';
} else if (
str.endsWith('s') ||
str.endsWith('x') ||
str.endsWith('z') ||
str.endsWith('ch') ||
str.endsWith('sh')
) {
return str + 'es';
} else {
return str + 's';
}
}
_getAvailableModels() {
const modelsPath = path.join(process.cwd(), 'backend/app/models');
if (!fs.existsSync(modelsPath)) {
return [];
}
// Define system models that should be excluded
const systemModels = ['jwt_denylist', 'otp'];
return fs.readdirSync(modelsPath)
.filter(file => file.endsWith('.rb'))
.map(file => file.replace('.rb', ''))
.filter(model => !systemModels.includes(model));
}
_removeMutationFolder(modelName) {
const mutationPath = this.destinationPath(`backend/app/graphql/mutations/${modelName}_mutations`);
if (fs.existsSync(mutationPath)) {
this.log(`Removing mutation folder: ${mutationPath}`);
fs.rmSync(mutationPath, { recursive: true, force: true });
}
}
_removeQueryFolder(modelName) {
const pluralModel = this._pluralize(modelName);
const queryPath = this.destinationPath(`backend/app/graphql/queries/${pluralModel}_queries`);
if (fs.existsSync(queryPath)) {
this.log(`Removing query folder: ${queryPath}`);
fs.rmSync(queryPath, { recursive: true, force: true });
}
}
_removeFrontendGraphQLFiles(modelName) {
const lowerModelName = modelName.toLowerCase();
// Remove frontend mutation files
const frontendMutationsPath = path.join(this.graphqlPath, 'mutations', lowerModelName);
if (fs.existsSync(frontendMutationsPath)) {
this.log(`Removing frontend mutations folder: ${frontendMutationsPath}`);
fs.rmSync(frontendMutationsPath, { recursive: true, force: true });
}
// Remove frontend query files
const frontendQueriesPath = path.join(this.graphqlPath, 'queries', lowerModelName);
if (fs.existsSync(frontendQueriesPath)) {
this.log(`Removing frontend queries folder: ${frontendQueriesPath}`);
fs.rmSync(frontendQueriesPath, { recursive: true, force: true });
}
}
_updateMutationType(modelName) {
const mutationTypePath = this.destinationPath('backend/app/graphql/types/mutation_type.rb');
if (fs.existsSync(mutationTypePath)) {
let content = fs.readFileSync(mutationTypePath, 'utf8');
// Fix malformed end statements (if any)
content = content.replace(/(\S+.*?)\s+end(\s*)$/gm, '$1\n end$2');
const capitalizedModel = _.upperFirst(_.camelCase(modelName));
// Mutation fields to remove
const mutationFields = [
`field :create${capitalizedModel},`,
`field :update${capitalizedModel},`,
`field :delete${capitalizedModel},`
];
let lines = content.split('\n');
let newLines = [];
let i = 0;
while (i < lines.length) {
const line = lines[i];
let shouldRemove = false;
// Check if the current line contains one of the mutation fields
for (const mf of mutationFields) {
if (line.includes(mf)) {
shouldRemove = true;
break;
}
}
if (shouldRemove) {
// If the next line exists and is exactly 'end' (trimmed), insert a blank line
if (i + 1 < lines.length && lines[i + 1].trim() === 'end') {
newLines.push('');
}
i++;
continue;
} else {
newLines.push(line);
i++;
}
}
// Join the lines and trim trailing whitespace/newlines
content = newLines.join('\n').trimEnd() + '\n';
// Remove extra blank lines immediately before any line that is exactly "end"
let finalLines = content.split('\n');
for (let j = 0; j < finalLines.length - 1; j++) {
if (finalLines[j].trim() === '' && finalLines[j + 1].trim() === 'end') {
finalLines.splice(j, 1);
j--; // adjust index after removal
}
}
content = finalLines.join('\n') + '\n';
fs.writeFileSync(mutationTypePath, content);
this.log(`Updated mutation type file: ${mutationTypePath}`);
}
}
_updateQueryType(modelName) {
const queryTypePath = this.destinationPath('backend/app/graphql/types/query_type.rb');
if (!fs.existsSync(queryTypePath)) return;
let content = fs.readFileSync(queryTypePath, 'utf8');
const capitalizedModel = _.upperFirst(_.camelCase(modelName));
// Compute plural using the camelCase form so "shift_interest" becomes "shiftInterest" then pluralized to "shiftInterests"
const pluralModel = this._pluralize(_.camelCase(modelName));
const capitalizedPluralModel = _.upperFirst(pluralModel);
// Split the content into lines for precise manipulation
let lines = content.split('\n');
// Identify lines to remove for list and show fields for this model
const linesToRemove = [];
lines.forEach((line, index) => {
if (
line.includes(`field :list${capitalizedPluralModel}`) ||
line.includes(`field :show${capitalizedModel}`)
) {
linesToRemove.push(index);
}
});
// Remove the identified lines in reverse order
linesToRemove.sort((a, b) => b - a).forEach(index => {
lines.splice(index, 1);
});
// Remove any "Generated queries" comment if no generated fields remain
const hasGeneratedQueries = lines.some(line =>
line.includes('field :list') || line.includes('field :show')
);
if (!hasGeneratedQueries) {
const commentIndex = lines.findIndex(line => line.includes('# Generated queries'));
if (commentIndex !== -1) {
lines.splice(commentIndex, 1);
}
}
// Clean up consecutive blank lines
for (let i = lines.length - 1; i > 0; i--) {
if (lines[i].trim() === '' && lines[i - 1].trim() === '') {
lines.splice(i, 1);
}
}
// Fix indentation for the class and module end statements
let classEndIndex = -1;
let moduleEndIndex = -1;
for (let i = lines.length - 1; i >= 0; i--) {
if (lines[i].trim() === 'end') {
if (moduleEndIndex === -1) {
moduleEndIndex = i;
} else if (classEndIndex === -1) {
classEndIndex = i;
break;
}
}
}
if (classEndIndex !== -1 && moduleEndIndex !== -1) {
lines[classEndIndex] = ' end';
lines[moduleEndIndex] = 'end';
}
content = lines.join('\n');
if (!content.endsWith('\n')) {
content += '\n';
}
fs.writeFileSync(queryTypePath, content);
this.log(`Updated query type file: ${queryTypePath}`);
}
async prompting() {
const availableModels = this._getAvailableModels();
if (availableModels.length === 0) {
this.log.error('No models found in backend/app/models');
return;
}
this.answers = await this.prompt([
{
type: 'checkbox',
name: 'modelNames',
message: 'Select the model(s) you want to remove GraphQL files for:',
choices: availableModels,
validate: input => input.length < 1 ? 'Select at least one model.' : true
},
{
type: 'confirm',
name: 'confirm',
message: answers => `Are you sure you want to remove all GraphQL files for the selected model(s)?`,
default: false
}
]);
}
writing() {
if (!this.answers.confirm) {
this.log('Operation cancelled');
return;
}
this.answers.modelNames.forEach(modelName => {
// Remove backend files
this._removeMutationFolder(modelName);
this._removeQueryFolder(modelName);
this._updateMutationType(modelName);
this._updateQueryType(modelName);
// Remove frontend files
this._removeFrontendGraphQLFiles(modelName);
this.log(`Successfully removed GraphQL files for model: ${modelName}`);
});
}
};