@codehance/rapid-stack
Version:
A modern full-stack development toolkit for rapid application development
308 lines (257 loc) • 9.82 kB
JavaScript
const Generator = require('yeoman-generator');
const path = require('path');
const fs = require('fs');
const yaml = require('js-yaml');
const utils = require('../../lib/utils');
module.exports = class extends Generator {
constructor(args, opts) {
super(args, opts);
this.backendPath = './backend';
this.schemaPath = path.join(this.backendPath, '_schema');
this.modelsPath = path.join(this.backendPath, 'app', 'models');
this.graphqlTypesPath = path.join(this.backendPath, 'app', 'graphql', 'types');
// Add debug option
this.option('debug', {
desc: 'Enable debug mode',
type: Boolean,
default: false
});
}
async prompting() {
// Get list of available schema files
const schemaFiles = this._getSchemaFiles();
if (schemaFiles.length === 0) {
this.log('No schema files found in the _schema directory.');
return;
}
// Ask which schema file to undo
this.answers = await this.prompt([
{
type: 'list',
name: 'schemaFile',
message: 'Which schema would you like to remove?',
choices: schemaFiles
},
{
type: 'confirm',
name: 'confirmRemove',
message: 'This will delete model files created by this schema. Continue?',
default: false
}
]);
if (!this.answers.confirmRemove) {
this.log('Schema removal cancelled.');
return;
}
// Load and parse the selected schema file
this.schemaData = this._loadSchemaFile(this.answers.schemaFile);
}
writing() {
if (!this.schemaData || !this.schemaData.models) {
return;
}
this.log(`Removing models from schema: ${this.answers.schemaFile}`);
// Process each model in the schema
Object.keys(this.schemaData.models).forEach(modelName => {
const modelData = this.schemaData.models[modelName];
if (!modelData || !modelData.attributes) {
return;
}
this.log(`Removing model: ${modelName}`);
// Process attributes to find enum fields
const enumFields = this._findEnumFields(modelData.attributes);
// Remove the model file
this._removeModelFile(modelName);
// Remove the GraphQL type file
this._removeGraphQLTypeFile(modelName);
// Remove any enum type files
this._removeEnumTypeFiles(modelName, enumFields);
});
this.log('Schema removal completed successfully!');
}
// Helper method to get list of schema files
_getSchemaFiles() {
if (!fs.existsSync(this.schemaPath)) {
return [];
}
return fs.readdirSync(this.schemaPath)
.filter(file => file.endsWith('.yml'))
.sort((a, b) => b.localeCompare(a)); // Sort in reverse order (newest first)
}
// Helper method to load and parse a schema file
_loadSchemaFile(filename) {
const filePath = path.join(this.schemaPath, filename);
try {
// Try the manual parsing approach first
const fileContent = fs.readFileSync(filePath, 'utf8');
if (this.options.debug) {
this.log('Original file content:');
this.log(fileContent);
}
// Parse the schema manually
const models = {};
let currentModel = null;
let inAttributes = false;
fileContent.split('\n').forEach(line => {
// Skip comments and empty lines
if (line.trim().startsWith('#') || !line.trim()) {
return;
}
// Check for model definition
const modelMatch = line.match(/^\s{2}([A-Za-z0-9]+):/);
if (modelMatch) {
currentModel = modelMatch[1];
models[currentModel] = { attributes: {} };
inAttributes = false;
return;
}
// Check for attributes section
if (line.match(/^\s{4}attributes:/) && currentModel) {
inAttributes = true;
return;
}
// Parse attribute
if (inAttributes && currentModel) {
const attrMatch = line.match(/^\s{6}([a-zA-Z0-9_]+):\s+(.*)/);
if (attrMatch) {
const [, fieldName, fieldValue] = attrMatch;
models[currentModel].attributes[fieldName] = fieldValue;
if (this.options.debug) {
this.log(`Parsed attribute: ${currentModel}.${fieldName} = ${fieldValue}`);
}
}
}
});
if (Object.keys(models).length > 0) {
if (this.options.debug) {
this.log('Parsed models:');
this.log(JSON.stringify(models, null, 2));
}
return { models };
}
// If manual parsing didn't work, try the YAML parser
this.log('Manual parsing did not find any models, trying YAML parser...');
// Preprocess the YAML content to handle the special format
// First, handle Enum fields with values and default
fileContent = fileContent.replace(/^(\s+)([a-zA-Z0-9_]+):\s+Enum\[(.*?)\](?:\(default:\s+(.*?)\))?(\s+required:\s+[a-zA-Z]+)?/gm,
(match, indent, fieldName, enumValues, defaultValue, required) => {
const requiredPart = required || '';
return `${indent}${fieldName}: "Enum[${enumValues}]${defaultValue ? `(default: ${defaultValue})` : ''}${requiredPart}"`;
}
);
// Then, handle "Type(default: value) required: true" format
fileContent = fileContent.replace(/^(\s+)([a-zA-Z0-9_]+):\s+([a-zA-Z]+)\(default:\s+([^)]+)\)(\s+required:\s+[a-zA-Z]+)/gm,
(match, indent, fieldName, fieldType, defaultValue, required) => {
return `${indent}${fieldName}: "${fieldType}(default: ${defaultValue})${required}"`;
}
);
// Finally, handle simple "Type required: true" format
fileContent = fileContent.replace(/^(\s+)([a-zA-Z0-9_]+):\s+([^:\n]+)(\s+required:\s+[a-zA-Z]+)/gm,
(match, indent, fieldName, fieldType, required) => {
// Skip if already processed (contains quotes)
if (fieldType.includes('"')) return match;
return `${indent}${fieldName}: "${fieldType.trim()}${required}"`;
}
);
if (this.options.debug) {
this.log('Preprocessed YAML:');
this.log(fileContent);
}
return yaml.load(fileContent);
} catch (error) {
this.log(`Error loading schema file: ${error.message}`);
return null;
}
}
// Helper method to find enum fields in attributes
_findEnumFields(attributes) {
const enumFields = [];
Object.keys(attributes).forEach(attrName => {
const attrValue = attributes[attrName];
if (typeof attrValue === 'string') {
// Check for various enum patterns
const isEnum =
attrValue.startsWith('Enum[') ||
attrValue.includes('Enum[') ||
attrValue.includes('enum_values') ||
// Check for specific enum field names that are commonly used
attrName === 'role' ||
attrName === 'status' ||
attrName.endsWith('_type') ||
attrName.endsWith('_status') ||
attrName.endsWith('_role');
if (isEnum) {
enumFields.push({
name: attrName
});
if (this.options.debug) {
this.log(`Detected enum field: ${attrName} with value: ${attrValue}`);
}
}
}
});
return enumFields;
}
// Helper method to remove a model file
_removeModelFile(modelName) {
const fileName = utils.camelToSnake(modelName);
const filePath = path.join(this.modelsPath, `${fileName}.rb`);
if (fs.existsSync(filePath)) {
try {
fs.unlinkSync(filePath);
this.log(`Removed model file: ${filePath}`);
} catch (error) {
this.log(`Error removing model file ${filePath}: ${error.message}`);
}
} else {
this.log(`Model file not found: ${filePath}`);
}
}
// Helper method to remove a GraphQL type file
_removeGraphQLTypeFile(modelName) {
const fileName = utils.camelToSnake(modelName);
const filePath = path.join(this.graphqlTypesPath, `${fileName}_type.rb`);
if (fs.existsSync(filePath)) {
try {
fs.unlinkSync(filePath);
this.log(`Removed GraphQL type file: ${filePath}`);
} catch (error) {
this.log(`Error removing GraphQL type file ${filePath}: ${error.message}`);
}
} else {
this.log(`GraphQL type file not found: ${filePath}`);
}
}
// Helper method to remove enum type files
_removeEnumTypeFiles(modelName, enumFields) {
if (!Array.isArray(enumFields) || enumFields.length === 0) {
return;
}
this.log(`Found ${enumFields.length} enum fields for ${modelName}`);
// Remove each enum type file
enumFields.forEach(field => {
const fileName = utils.camelToSnake(modelName);
const fieldName = utils.camelToSnake(field.name);
// Try both naming conventions
const filePaths = [
path.join(this.graphqlTypesPath, `${fileName}_${fieldName}_enum_type.rb`),
path.join(this.graphqlTypesPath, `${fileName}_${fieldName}_enum.rb`)
];
let fileRemoved = false;
filePaths.forEach(filePath => {
if (fs.existsSync(filePath)) {
try {
fs.unlinkSync(filePath);
this.log(`Removed enum type file: ${filePath}`);
fileRemoved = true;
} catch (error) {
this.log(`Error removing enum type file ${filePath}: ${error.message}`);
}
}
});
if (!fileRemoved) {
this.log(`Enum type file not found for ${modelName}.${field.name}`);
}
});
}
};