UNPKG

fhipster

Version:

A CLI tool to convert JHipster JDL to Flutter GetX services and models.

292 lines (246 loc) 10.6 kB
#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const yargs = require('yargs/yargs'); const { hideBin } = require('yargs/helpers'); function jdlToDartType(jdlType) { const typeMapping = { 'String': 'String', 'Integer': 'int', 'Long': 'int', 'Float': 'double', 'Double': 'double', 'BigDecimal': 'double', 'LocalDate': 'DateTime', 'Instant': 'DateTime', 'ZonedDateTime': 'DateTime', 'Boolean': 'bool', 'UUID': 'String' }; return typeMapping[jdlType] || 'dynamic'; } function parseJdl(jdlContent) { const entities = {}; // Updated regex to capture optional annotations before the entity definition // match[1] will contain the annotation string (e.g., "@EnableAudit ") if present // match[2] will be the entity name // match[3] will be the raw fields string inside the entity block const entityRegex = /(@\w+\s*)*entity\s+(\w+)\s*\{([^}]+)\}/g; let match; while ((match = entityRegex.exec(jdlContent)) !== null) { const annotations = match[1] || ''; // Get the captured annotations string const entityName = match[2]; let rawFieldsStr = match[3]; // Get the raw string inside the entity block // Step 1: Remove multi-line comments /* ... */ let cleanedFieldsStr = rawFieldsStr.replace(/\/\*[\s\S]*?\*\//g, ''); // Step 2: Remove single-line comments // ... to end of line, globally and across multiple lines cleanedFieldsStr = cleanedFieldsStr.replace(/\/\/.*$/gm, ''); // Step 3: Trim whitespace from each line and filter out empty lines const lines = cleanedFieldsStr.split('\n') .map(line => line.trim()) .filter(line => line.length > 0); const fields = []; // Regex to capture fieldName and Type, anchored to the start of the line. const fieldLineRegex = /^(\w+)\s+([\w<>]+)/; for (const line of lines) { const fieldMatch = line.match(fieldLineRegex); if (fieldMatch) { // fieldMatch[1] is the field name, fieldMatch[2] is the JDL type fields.push({ name: fieldMatch[1], type: fieldMatch[2] }); } } // Check if the @EnableAudit annotation is present for this entity if (annotations.includes('@EnableAudit')) { // Add the four audit fields if @EnableAudit is found fields.push( { name: 'lastModifiedDate', type: 'Instant' }, // JDL Instant maps to Dart DateTime { name: 'lastModifiedBy', type: 'String' }, { name: 'createdDate', type: 'Instant' }, // JDL Instant maps to Dart DateTime { name: 'createdBy', type: 'String' } ); } entities[entityName] = fields; } return entities; } function generateModelTemplate(entityName, fields) { const className = `${entityName}Model`; const fieldsDeclarations = fields .map(f => ` final ${jdlToDartType(f.type)}? ${f.name};`) .join('\n'); const constructorParams = fields .map(f => ` this.${f.name},`) .join('\n'); const fromJsonAssignments = fields .map(f => { const fieldName = f.name; const dartType = jdlToDartType(f.type); if (dartType === 'DateTime') { return ` ${fieldName}: json['${fieldName}'] != null ? DateTime.parse(json['${fieldName}']) : null,`; } return ` ${fieldName}: json['${fieldName}'],`; }) .join('\n'); const toJsonAssignments = fields .map(f => { const fieldName = f.name; const dartType = jdlToDartType(f.type); if (dartType === 'DateTime') { return ` '${fieldName}': ${fieldName}?.toIso8601String(),`; } return ` '${fieldName}': ${fieldName},`; }) .join('\n'); return `class ${className} { ${fieldsDeclarations} ${className}({ ${constructorParams} }); factory ${className}.fromJson(Map<String, dynamic> json) { return ${className}( ${fromJsonAssignments} ); } Map<String, dynamic> toJson() { return { ${toJsonAssignments} }; } } `; } /** * Converts a camelCase string to kebab-case. * @param {string} camelCaseString - The string in camelCase. * @returns {string} The string in kebab-case. */ function camelToKebabCase(camelCaseString) { return camelCaseString.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase(); } /** * Generates the content for a GetX service file. * @param {string} entityName - The name of the entity. * @param {string} microserviceName - The name of the microservice. * @param {string} apiHost - The base host for the API (e.g., 'https://api.yourapp.com'). * @returns {string} The Dart code for the service. */ function generateServiceTemplate(entityName, microserviceName, apiHost) { const modelClassName = `${entityName}Model`; const serviceClassName = `${entityName}Service`; const instanceName = entityName.charAt(0).toLowerCase() + entityName.slice(1); // First, determine the pluralized camelCase endpoint name let pluralCamelCaseEndpoint = instanceName.endsWith('y') ? `${instanceName.slice(0, -1)}ies` : `${instanceName}s`; // Then, convert the pluralized camelCase name to kebab-case const endpointName = camelToKebabCase(pluralCamelCaseEndpoint); // Construct the base URL using the provided apiHost const baseUrl = `'${apiHost}/services/${microserviceName}/api'`; return `import 'package:get/get.dart'; import '../models/${instanceName}_model.dart'; class ${serviceClassName} extends GetxService { final _apiClient = GetConnect(timeout: Duration(seconds: 30)); // IMPORTANT: Replace the host part if 'apiHost' argument was not used, or adjust as needed. final String _baseUrl = ${baseUrl}; @override void onInit() { _apiClient.baseUrl = _baseUrl; super.onInit(); } Future<List<${modelClassName}>> getAll() async { final response = await _apiClient.get('/${endpointName}'); if (response.status.hasError) { return Future.error(response.statusText!); } return (response.body as List).map((json) => ${modelClassName}.fromJson(json)).toList(); } Future<${modelClassName}> getById(int id) async { final response = await _apiClient.get('/${endpointName}/$id'); if (response.status.hasError) { return Future.error(response.statusText!); } return ${modelClassName}.fromJson(response.body); } Future<${modelClassName}> create(${modelClassName} ${instanceName}) async { final response = await _apiClient.post('/${endpointName}', ${instanceName}.toJson()); if (response.status.hasError) { return Future.error(response.statusText!); } return ${modelClassName}.fromJson(response.body); } Future<${modelClassName}> update(${modelClassName} ${instanceName}) async { if (${instanceName}.id == null) { return Future.error("Cannot update a model without an ID."); } final response = await _apiClient.put('/${endpointName}/${'$'}{${instanceName}.id}', ${instanceName}.toJson()); if (response.status.hasError) { return Future.error(response.statusText!); } return ${modelClassName}.fromJson(response.body); } Future<void> delete(int id) async { final response = await _apiClient.delete('/${endpointName}/$id'); if (response.status.hasError) { return Future.error(response.statusText!); } } } `; } function main() { const argv = yargs(hideBin(process.argv)) .usage('Usage: $0 <jdlFile> --microservice <name> [--apiHost <host>] [outputDir]') .demandCommand(1, 'You must provide the path to the JDL file.') .option('microservice', { alias: 'm', description: 'The name of the microservice (e.g., dms).', type: 'string', demandOption: true, }) .option('apiHost', { alias: 'a', description: 'The base host for the API (e.g., https://your-domain.com). Defaults to https://api.yourapp.com.', type: 'string', default: 'https://api.yourapp.com', // Default API host }) .help('h') .alias('h', 'help') .argv; const jdlFilePath = argv._[0]; const microserviceName = argv.microservice; const apiHost = argv.apiHost; // Get the apiHost from arguments const outputDir = argv._[1] || 'flutter_generated'; if (!fs.existsSync(jdlFilePath)) { console.error(`Error: JDL file not found at '${jdlFilePath}'`); process.exit(1); } const jdlContent = fs.readFileSync(jdlFilePath, 'utf8'); const entities = parseJdl(jdlContent); if (Object.keys(entities).length === 0) { console.log('No entities found in the JDL file.'); return; } const modelsDir = path.join(outputDir, 'models'); const servicesDir = path.join(outputDir, 'services'); fs.mkdirSync(modelsDir, { recursive: true }); fs.mkdirSync(servicesDir, { recursive: true }); console.log(`Generating files in '${outputDir}' directory for microservice '${microserviceName}' with API host '${apiHost}'...`); for (const [entityName, fields] of Object.entries(entities)) { console.log(`- Processing entity: ${entityName}`); const instanceName = entityName.charAt(0).toLowerCase() + entityName.slice(1); const modelContent = generateModelTemplate(entityName, fields); const modelPath = path.join(modelsDir, `${instanceName}_model.dart`); fs.writeFileSync(modelPath, modelContent); // Pass apiHost to generateServiceTemplate const serviceContent = generateServiceTemplate(entityName, microserviceName, apiHost); const servicePath = path.join(servicesDir, `${instanceName}_service.dart`); fs.writeFileSync(servicePath, serviceContent); } console.log('\n✅ Generation complete!'); console.log('Next steps:'); console.log(`1. Copy the '${outputDir}' folder into your Flutter project's 'lib' directory.`); console.log("2. Add 'get' to your 'pubspec.yaml': dependencies:\n get: ^4.6.5"); console.log("3. IMPORTANT: The '_baseUrl' in your generated service files is now set to include your microservice name and the specified API host."); console.log("4. Register your services in your GetX dependency injection setup."); } main();