@codehance/rapid-stack
Version:
A modern full-stack development toolkit for rapid application development
911 lines (778 loc) • 30.4 kB
JavaScript
const Generator = require('yeoman-generator');
const path = require('path');
const fs = require('fs');
const _ = require('lodash');
const utils = require('../../lib/utils');
const { handlePrompt } = require('../../lib/utils');
module.exports = class extends Generator {
constructor(args, opts) {
super(args, opts);
this.option('fields', { type: String });
this.option('timestamps', { type: Boolean, default: true });
// Add frontend paths
this.frontendPath = './frontend';
this.graphqlPath = 'frontend/src/app/graphql';
}
// Helper method for proper pluralization
_pluralize(str) {
return utils.pluralize(str);
}
_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', 'two_factor'];
// Get all models and filter out system models
const models = fs.readdirSync(modelsPath)
.filter(file => file.endsWith('.rb'))
.map(file => file.replace('.rb', ''))
.filter(model => !systemModels.includes(model));
// If 'user' exists in the models, move it to the top
if (models.includes('user')) {
const userIndex = models.indexOf('user');
models.splice(userIndex, 1); // Remove 'user' from its current position
models.unshift('user'); // Add 'user' to the beginning of the array
}
return models;
}
_getModelFields(modelName) {
const modelPath = path.join(process.cwd(), 'backend/app/models', `${modelName}.rb`);
if (!fs.existsSync(modelPath)) {
return [];
}
const content = fs.readFileSync(modelPath, 'utf8');
const fields = [];
// Extract all field definitions with a more flexible approach
const modelContent = content.toString();
const fieldRegex = /field\s+:(\w+)/g;
let fieldMatch;
while ((fieldMatch = fieldRegex.exec(modelContent)) !== null) {
const fieldName = fieldMatch[1];
// Skip internal Mongoid fields and timestamps
if (!['_id', 'created_at', 'updated_at'].includes(fieldName)) {
const lineRegex = new RegExp(`field\\s+:${fieldName}.*$`, 'm');
const lineMatch = modelContent.match(lineRegex);
let fieldType = 'String'; // Default to String
if (lineMatch) {
const typeMatch = lineMatch[0].match(/type:\s+(\w+)/);
if (typeMatch) {
fieldType = typeMatch[1];
}
}
fields.push({
name: fieldName,
type: this._convertMongoTypeToGraphQL(fieldType)
});
}
}
// Extract enum fields
const enumsMatch = modelContent.match(/ENUMS\s*=\s*\{([^}]+)\}/s);
if (enumsMatch) {
const enumsContent = enumsMatch[1];
const enumFieldRegex = /(\w+):/g;
let enumFieldMatch;
while ((enumFieldMatch = enumFieldRegex.exec(enumsContent)) !== null) {
const enumFieldName = enumFieldMatch[1];
if (!fields.some(f => f.name === enumFieldName)) {
fields.push({
name: enumFieldName,
type: 'String'
});
}
}
}
// Extract belongs_to relationships
const belongsToRegex = /belongs_to\s+:(\w+)/g;
let relationMatch;
while ((relationMatch = belongsToRegex.exec(modelContent)) !== null) {
const relationName = relationMatch[1];
if (relationName === 'company') {
continue;
}
const relationIdField = `${relationName}_id`;
if (!fields.some(f => f.name === relationIdField)) {
fields.push({
name: relationIdField,
type: 'ID',
isRelationship: true
});
}
}
return fields;
}
async prompting() {
const availableModels = this._getAvailableModels();
const permissionLevels = [
'ensure_all_groups',
'admin_or_platform_admin',
'admin',
'platform_admin'
];
this.answers = await handlePrompt(this, [
{
type: 'list',
name: 'mutationType',
message: 'What type of operation would you like to generate?',
choices: ['crud', 'single']
},
{
type: 'list',
name: 'operationType',
message: 'What type of operation would you like to generate?',
choices: ['mutation', 'query'],
when: (answers) => answers.mutationType === 'single'
},
{
type: 'checkbox',
name: 'models',
message: 'Select the model(s):',
choices: availableModels,
validate: input => input.length < 1 ? 'Select at least one model.' : true,
default: this.options.askAnswered ? availableModels : [availableModels[0]]
}
]);
// Build an array of selected model objects with extra details.
this.selectedModels = [];
for (const model of this.answers.models) {
// Use the original model name (in snake_case) for file paths
const baseName = model;
const capitalizedName = _.upperFirst(_.camelCase(model));
this.log(`\n=== Creating permissions for ${capitalizedName} ===\n`);
const modelFields = this._getModelFields(model);
let operations = {};
let crudPermissions = {};
if (this.answers.mutationType === 'crud') {
const crudOperations = ['create', 'update', 'delete', 'list', 'show'];
for (const operation of crudOperations) {
const { permission } = await handlePrompt(this, [
{
type: 'list',
name: 'permission',
message: `Select permission level for ${operation} ${baseName}:`,
choices: permissionLevels
}
]);
crudPermissions[operation] = permission;
}
operations = {
create: `create${capitalizedName}`,
update: `update${capitalizedName}`,
delete: `delete${capitalizedName}`,
list: `list${capitalizedName}s`,
show: `show${capitalizedName}`
};
} else {
const { permission, name } = await handlePrompt(this, [
{
type: 'list',
name: 'permission',
message: 'Select the permission level:',
choices: permissionLevels
},
{
type: 'input',
name: 'name',
message: this.answers.operationType === 'mutation' ?
`Enter the mutation name for ${baseName} using camelCase:` :
`Enter the query name for ${baseName} using camelCase:`
}
]);
crudPermissions = { single: permission };
operations = { single: name };
}
this.selectedModels.push({ model, baseName, capitalizedName, modelFields, operations, crudPermissions });
}
}
writing() {
if (this.answers.mutationType === 'crud') {
for (const modelObj of this.selectedModels) {
// Generate backend files
this._generateCrudMutations(modelObj);
// Generate frontend files
this._generateFrontendGraphQLFiles(modelObj.model, modelObj.modelFields.map(f => f.name));
}
} else {
if (this.answers.operationType === 'mutation') {
for (const modelObj of this.selectedModels) {
this._generateSingleMutation(modelObj);
}
} else {
for (const modelObj of this.selectedModels) {
this._generateSingleQuery(modelObj);
}
}
}
this.log('Created GraphQL files successfully!');
}
_generateFrontendGraphQLFiles(modelName, fields) {
const snakeCaseModelName = _.snakeCase(modelName);
const camelCaseModelName = _.camelCase(modelName);
const queriesPath = path.join(this.graphqlPath, 'queries', snakeCaseModelName);
const mutationsPath = path.join(this.graphqlPath, 'mutations', snakeCaseModelName);
fs.mkdirSync(queriesPath, { recursive: true });
fs.mkdirSync(mutationsPath, { recursive: true });
const { fields: regularFields, relationshipFields } = this._getGraphQLTypeFields(modelName);
// Define sensitive fields to exclude for User model
const sensitiveUserFields = [
'encrypted_password',
'reset_password_token',
'reset_password_sent_at',
'remember_created_at',
'reset_password_token_expires_at',
'role'
];
// Filter out sensitive fields if this is the User model
const filteredRegularFields = modelName === 'user'
? regularFields.filter(field => {
const fieldSnakeCase = _.snakeCase(field.name);
return !sensitiveUserFields.includes(fieldSnakeCase);
})
: regularFields;
// Only use regular fields, exclude relationship fields completely
const regularFieldsString = filteredRegularFields
.map(field => _.camelCase(field.name))
.join('\n ');
// Filter only belongs_to relationships and format them with just the ID
const belongsToRelationships = relationshipFields
.filter(field => field.relationshipType === 'belongsTo')
.map(field => `${_.camelCase(field.name)} { id }`)
.join('\n ');
// Combine regular fields and belongsTo relationships
const allFieldsString = `${regularFieldsString}${belongsToRelationships ? '\n ' + belongsToRelationships : ''}`;
// Use plural form for the list query
const pluralCamelCaseModelName = this._pluralize(camelCaseModelName);
const listQueryContent = `import { gql } from 'apollo-angular';
export const List${this._capitalize(pluralCamelCaseModelName)}Query = gql\`
query list${this._capitalize(pluralCamelCaseModelName)}($page: Int, $perPage: Int, $orderDirection: String, $filters: JSON) {
list${this._capitalize(pluralCamelCaseModelName)}(page: $page, perPage: $perPage, orderDirection: $orderDirection, filters: $filters) {
data {
id
${allFieldsString}
createdAt
updatedAt
}
errors
message
httpStatus
options {
totalPages
totalCount
currentPage
perPage
prevPage
nextPage
}
}
}
\`;`;
// Use plural form for the list query file name
const listQueryFile = path.join(queriesPath, `list${this._capitalize(pluralCamelCaseModelName)}.query.ts`);
this._writeFile(listQueryFile, listQueryContent);
this.log(` create ${listQueryFile}`);
const showQueryContent = `import { gql } from 'apollo-angular';
export const Show${this._capitalize(camelCaseModelName)}Query = gql\`
query show${this._capitalize(camelCaseModelName)}($id: ID!) {
show${this._capitalize(camelCaseModelName)}(id: $id) {
data {
id
${allFieldsString}
createdAt
updatedAt
}
errors
message
httpStatus
}
}
\`;`;
const showQueryFile = path.join(queriesPath, `show${this._capitalize(camelCaseModelName)}.query.ts`);
this._writeFile(showQueryFile, showQueryContent);
this.log(` create ${showQueryFile}`);
['create', 'update', 'delete'].forEach(operation => {
const mutationContent = this._processTemplate(`graphql/${operation}.mutation.ts.ejs`, {
modelName: camelCaseModelName,
fields: regularFields,
allFields: regularFields,
regularFields: regularFields,
relationshipFields: relationshipFields.filter(field => field.relationshipType === 'belongsTo'),
h: {
toPascalCase: this._toPascalCaseWithCompoundWords.bind(this),
capitalize: this._capitalize.bind(this),
toCamelCase: this._camelCase.bind(this)
}
});
const mutationFile = path.join(mutationsPath, `${operation}${this._capitalize(camelCaseModelName)}.mutation.ts`);
this._writeFile(mutationFile, mutationContent);
this.log(` create ${mutationFile}`);
});
}
_ensureBaseQueryExists() {
const baseQueryDir = 'backend/app/graphql/queries';
const baseQueryPath = path.join(baseQueryDir, 'base_query.rb');
if (!fs.existsSync(baseQueryDir)) {
fs.mkdirSync(baseQueryDir, { recursive: true });
}
if (!fs.existsSync(baseQueryPath)) {
const content = `# frozen_string_literal: true
module Queries
# Base query class
class BaseQuery < GraphQL::Schema::Resolver
include SharedGraphqlMethods
include ServiceResponse
include Paginatable
# Get the current user
def current_user
context[:current_user]
end
# Get the current user's company
def company
current_user.company
end
end
end`;
fs.writeFileSync(baseQueryPath, content);
}
}
_generateSingleMutation(modelObj) {
const { baseName, capitalizedName, modelFields, operations, crudPermissions } = modelObj;
const opName = _.camelCase(operations.single);
const snakeCaseName = _.snakeCase(modelObj.model);
const snakeCaseOpName = this._toSnakeCase(opName);
const modelName = capitalizedName;
const mutationDir = `backend/app/graphql/mutations/${snakeCaseName}_mutations`;
const isCreate = opName.toLowerCase().startsWith('create');
const isUpdate = opName.toLowerCase().startsWith('update');
const isDelete = opName.toLowerCase().startsWith('delete');
let fields = [...modelFields];
if (isUpdate || isDelete) {
fields.unshift({ name: 'id', type: 'ID' });
}
if (isUpdate) {
fields = fields.map(field => ({ ...field, required: false }));
}
if (isDelete) {
fields = fields.slice(0, 1);
}
this.fs.copyTpl(
this.templatePath('single/mutation.rb'),
this.destinationPath(`${mutationDir}/${snakeCaseOpName}.rb`),
{
name: _.upperFirst(opName),
modelName,
operationName: opName,
fields,
permission: crudPermissions.single,
isCreate,
isUpdate,
isDelete
}
);
this._updateMutationType(modelObj);
}
_generateSingleQuery(modelObj) {
this.log(`Generating single query for ${modelObj.baseName} is not yet implemented.`);
}
_convertMongoTypeToGraphQL(mongoType) {
const typeMap = {
'String': 'String',
'Integer': 'Int',
'Float': 'Float',
'Boolean': 'Boolean',
'Time': 'String',
'Date': 'String',
'DateTime': 'String',
'Array': 'String',
'Hash': 'String',
'Object': 'String'
};
return typeMap[mongoType] || 'String';
}
_generateMutationContent(mutationName, modelName) {
const capitalizedMutationName = this._capitalize(mutationName);
const capitalizedModelName = this._capitalize(modelName);
const fields = this._getModelFields(modelName);
const argumentDefinitions = fields
.map(field => ` argument :${field.name}, ${field.type}, required: true`)
.join('\n');
const resolveParams = fields
.map(field => `${field.name}:`)
.join(', ');
const permissionCode = this.answers.permission !== 'none'
? `\n require_permission :${this.answers.permission}\n\n before_action :check_permission\n`
: '';
return `# frozen_string_literal: true
module Mutations
module ${capitalizedModelName}Mutations
# ${capitalizedMutationName} mutation
class ${capitalizedMutationName} < Mutations::BaseMutation
# Define the input arguments for the mutation
${argumentDefinitions}
return_type Types::${capitalizedModelName}Type${permissionCode}
def resolve(${resolveParams})
super
# Add your mutation logic here
end
end
end
end
`;
}
_toSnakeCase(str) {
return str
.replace(/([A-Z])/g, '_$1')
.toLowerCase()
.replace(/^_/, '');
}
_updateMutationType(modelObj) {
const mutationTypePath = path.join(process.cwd(), 'backend/app/graphql/types/mutation_type.rb');
if (fs.existsSync(mutationTypePath)) {
let content = fs.readFileSync(mutationTypePath, 'utf8');
content = content.replace(/(\S+.*?)\s+end(\s*)$/gm, '$1\n end$2');
let lines = content.split('\n');
const classLineIndex = lines.findIndex(line => line.includes('class MutationType'));
if (classLineIndex === -1) {
this.log.error('Could not find class definition');
return;
}
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.trim().includes(' end') && !line.trim().startsWith('end')) {
const parts = line.split(/\s+end\s*/);
if (parts.length > 1) {
lines[i] = parts[0];
lines.splice(i + 1, 0, ' end');
}
}
}
const lastFieldIndex = lines.findLastIndex((line, index) =>
line.trim().startsWith('field :')
);
const insertIndex = lastFieldIndex !== -1 ? lastFieldIndex + 1 : classLineIndex + 1;
let newFields = [];
if (this.answers.mutationType === 'crud') {
const { baseName, capitalizedName, operations, crudPermissions } = modelObj;
const mutationsToAdd = ['create', 'update', 'delete'];
mutationsToAdd.forEach(operation => {
const mutationName = operations[operation];
if (!lines.some(line => line.includes(`field :${mutationName},`))) {
newFields.push(
` field :${mutationName}, mutation: Mutations::${capitalizedName}Mutations::${_.upperFirst(mutationName)}`
);
}
});
} else if (this.answers.operationType === 'mutation') {
const mutationName = modelObj.operations.single;
const capName = _.upperFirst(mutationName);
const modelName = modelObj.capitalizedName;
if (!lines.some(line => line.includes(`field :${mutationName},`))) {
newFields.push(
` field :${mutationName}, mutation: Mutations::${modelName}Mutations::${capName}`
);
}
}
let classEndIndex = lines.findIndex((line, index) =>
line.trim() === 'end' && index > classLineIndex
);
let moduleEndIndex = lines.findIndex((line, index) =>
line.trim() === 'end' && index > (classEndIndex !== -1 ? classEndIndex : classLineIndex)
);
if (classEndIndex === -1) {
lines.push(' end');
classEndIndex = lines.length - 1;
}
if (moduleEndIndex === -1) {
lines.push('end');
moduleEndIndex = lines.length - 1;
}
if (lastFieldIndex !== -1 && classEndIndex === lastFieldIndex + 1) {
lines.splice(classEndIndex, 0, '');
classEndIndex++;
if (moduleEndIndex !== -1) moduleEndIndex++;
}
if (newFields.length > 0) {
if (lastFieldIndex !== -1) {
newFields.unshift('');
}
lines = [
...lines.slice(0, insertIndex),
...newFields,
...lines.slice(insertIndex, classEndIndex - 1),
'',
...lines.slice(classEndIndex)
];
}
content = lines.join('\n') + '\n';
fs.writeFileSync(mutationTypePath, content);
}
}
_updateQueryType(modelObj) {
// Update query_type.rb for the given model
const queryTypePath = path.join(process.cwd(), 'backend/app/graphql/types/query_type.rb');
if (!fs.existsSync(queryTypePath)) return;
let content = fs.readFileSync(queryTypePath, 'utf8');
const lines = content.split('\n');
let endCount = 0;
let classEndIndex = lines.length - 1;
for (let i = lines.length - 1; i >= 0; i--) {
if (lines[i].trim() === 'end') {
endCount++;
if (endCount === 2) {
classEndIndex = i;
break;
}
}
}
const { baseName, capitalizedName } = modelObj;
// Properly handle PascalCase for compound words
const pluralBaseName = this._pluralize(baseName);
// First convert to camelCase, then ensure proper PascalCase with capitalized words
const camelCasePluralName = _.camelCase(pluralBaseName);
// For compound words like "shiftInterest", we need to ensure each word is capitalized
// This will convert "shiftinterest" to "ShiftInterest"
const pascalCasePluralName = this._toPascalCaseWithCompoundWords(camelCasePluralName);
const listFieldName = 'list' + pascalCasePluralName;
const showFieldName = 'show' + capitalizedName;
const newFields = [
` field :${listFieldName}, resolver: Queries::${pascalCasePluralName}Queries::List${pascalCasePluralName}`,
` field :${showFieldName}, resolver: Queries::${pascalCasePluralName}Queries::Show${capitalizedName}`
];
const fieldsToInsert = newFields.filter(field => !content.includes(field));
if (fieldsToInsert.length > 0) {
lines.splice(classEndIndex, 0, ...fieldsToInsert, '');
content = lines.join('\n');
fs.writeFileSync(queryTypePath, content);
}
}
// Helper method to convert camelCase to PascalCase with proper handling of compound words
_toPascalCaseWithCompoundWords(str) {
// Handle common compound words that should be properly capitalized
const compoundWords = {
'shiftinterest': 'ShiftInterest',
'shiftinterests': 'ShiftInterests',
'userbranch': 'UserBranch',
'userbranches': 'UserBranches'
};
// Check if the string is a known compound word
if (compoundWords[str.toLowerCase()]) {
return compoundWords[str.toLowerCase()];
}
// Otherwise, use a general approach to capitalize each word
// First, ensure the first letter is capitalized
let result = str.charAt(0).toUpperCase() + str.slice(1);
// Then find word boundaries in camelCase and capitalize them
// This regex looks for lowercase letters followed by uppercase letters
// and inserts a space between them, then capitalizes each word
result = result.replace(/([a-z])([A-Z])/g, '$1 $2')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
return result;
}
_capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
_camelCase(str) {
return _.camelCase(str);
}
// Helper method to process template content
_processTemplate(templatePath, data) {
try {
const templateContent = fs.readFileSync(this.templatePath(templatePath), 'utf8');
const utils = require('../../lib/utils');
const templateData = {
...data,
h: {
capitalize: this._capitalize.bind(this),
pluralize: utils.pluralize,
toCamelCase: utils.toCamelCase,
toSnakeCase: utils.toSnakeCase,
toPascalCase: utils.toPascalCase,
camelToSnake: utils.camelToSnake
}
};
const ejs = require('ejs');
return ejs.render(templateContent, templateData);
} catch (error) {
this.log.error(`Error processing template ${templatePath}:`, error);
return '';
}
}
// Helper method to write or update file
_writeFile(filePath, content) {
try {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, content, 'utf8');
} catch (error) {
this.log.error(`Error writing file ${filePath}:`, error);
}
}
_generateCrudMutations(modelObj) {
const { baseName, capitalizedName, modelFields, operations, crudPermissions } = modelObj;
// Use snake_case for file names but CamelCase for module names
const snakeCaseName = _.snakeCase(modelObj.model);
const pluralSnakeCaseName = this._pluralize(snakeCaseName);
// Use proper PascalCase for directory names
const pluralizedDirName = `${pluralSnakeCaseName}_queries`;
const moduleBaseName = _.camelCase(modelObj.model);
const moduleName = _.upperFirst(moduleBaseName);
// Ensure proper PascalCase for module names with compound words
const camelCasePluralName = _.camelCase(this._pluralize(moduleBaseName));
const pluralModuleName = this._toPascalCaseWithCompoundWords(camelCasePluralName);
// Ensure base query file exists
this._ensureBaseQueryExists();
// Create directories using snake_case names
const mutationDir = `backend/app/graphql/mutations/${snakeCaseName}_mutations`;
const queryDir = `backend/app/graphql/queries/${pluralizedDirName}`;
if (!fs.existsSync(mutationDir)) {
fs.mkdirSync(mutationDir, { recursive: true });
}
if (!fs.existsSync(queryDir)) {
fs.mkdirSync(queryDir, { recursive: true });
}
// Generate mutations (create, update, delete)
const mutations = ['create', 'update', 'delete'];
mutations.forEach(operation => {
const templatePath = this.templatePath(`crud/${operation}.rb`);
const destinationPath = this.destinationPath(`${mutationDir}/${operation}_${snakeCaseName}.rb`);
console.log(`Generating ${operation} mutation for ${capitalizedName}`);
console.log(`Template path: ${templatePath}`);
console.log(`Destination path: ${destinationPath}`);
// For create and update operations, we need to include all fields
// For delete, we only need the ID field
let fieldsToUse = [...modelFields];
if (operation === 'update' || operation === 'delete') {
// Add ID field for update and delete operations
fieldsToUse.unshift({ name: 'id', type: 'ID' });
}
if (operation === 'update') {
// For update, all fields are optional
fieldsToUse = fieldsToUse.map(field => ({ ...field, required: false }));
}
if (operation === 'delete') {
// For delete, we only need the ID field
fieldsToUse = fieldsToUse.slice(0, 1);
}
// Generate the mutation file
this.fs.copyTpl(
templatePath,
destinationPath,
{
name: capitalizedName,
baseName: moduleBaseName,
operationName: operations[operation],
fields: fieldsToUse,
permission: crudPermissions[operation],
moduleName,
pluralName: pluralModuleName,
h: {
capitalize: this._capitalize.bind(this)
}
}
);
});
// Generate queries (list, show)
const queries = ['list', 'show'];
queries.forEach(operation => {
const fileName = operation === 'list' ?
`list_${pluralSnakeCaseName}.rb` :
`show_${snakeCaseName}.rb`;
// Create the proper module name for the query file (PascalCase with compound words)
const moduleNameForFile = pluralModuleName;
this.fs.copyTpl(
this.templatePath(`crud/${operation}.rb`),
this.destinationPath(`${queryDir}/${fileName}`),
{
name: capitalizedName,
baseName: moduleBaseName,
pluralName: pluralModuleName,
capitalizedPluralName: moduleNameForFile,
operationName: operations[operation],
fields: modelFields,
permission: crudPermissions[operation],
moduleName: moduleNameForFile,
h: {
capitalize: this._capitalize.bind(this)
}
}
);
});
// Update mutation_type.rb and query_type.rb with the new fields
this._updateMutationType(modelObj);
this._updateQueryType(modelObj);
}
_getGraphQLTypeFields(modelName) {
const snakeCaseModelName = _.snakeCase(modelName);
const typeFilePath = path.join(process.cwd(), 'backend/app/graphql/types', `${snakeCaseModelName}_type.rb`);
if (!fs.existsSync(typeFilePath)) {
return { fields: [], relationshipFields: [] };
}
// First, let's get the model file to identify actual relationships
const modelPath = path.join(process.cwd(), 'backend/app/models', `${modelName}.rb`);
let modelContent = '';
if (fs.existsSync(modelPath)) {
modelContent = fs.readFileSync(modelPath, 'utf8').toString();
}
// Extract relationship definitions from the model
const relationships = {
belongsTo: [],
hasMany: [],
hasOne: []
};
// Extract belongs_to relationships
const belongsToRegex = /belongs_to\s+:(\w+)/g;
let belongsToMatch;
while ((belongsToMatch = belongsToRegex.exec(modelContent)) !== null) {
relationships.belongsTo.push(belongsToMatch[1]);
}
// Extract has_many relationships
const hasManyRegex = /has_many\s+:(\w+)/g;
let hasManyMatch;
while ((hasManyMatch = hasManyRegex.exec(modelContent)) !== null) {
relationships.hasMany.push(hasManyMatch[1]);
}
// Extract has_one relationships
const hasOneRegex = /has_one\s+:(\w+)/g;
let hasOneMatch;
while ((hasOneMatch = hasOneRegex.exec(modelContent)) !== null) {
relationships.hasOne.push(hasOneMatch[1]);
}
// Now process the GraphQL type file
const content = fs.readFileSync(typeFilePath, 'utf8');
const fields = [];
const relationshipFields = [];
const fieldRegex = /field\s+:(\w+),\s+([^,\n]+)(?:,\s+null:\s+(\w+))?/g;
let match;
while ((match = fieldRegex.exec(content)) !== null) {
const [, fieldName, fieldType] = match;
if (!['id', 'created_at', 'updated_at'].includes(fieldName)) {
if (fieldType.trim().startsWith('GraphQL::Types')) {
fields.push({
name: fieldName,
type: fieldType.trim(),
isRelationship: false
});
} else if (fieldType.includes('Types::')) {
// Process as a relationship field as before
const isBelongsTo = relationships.belongsTo.includes(fieldName);
const isHasMany = relationships.hasMany.includes(fieldName);
const isHasOne = relationships.hasOne.includes(fieldName);
relationshipFields.push({
name: fieldName,
type: fieldType.trim(),
isRelationship: true,
relationshipType: isBelongsTo ? 'belongsTo' : (isHasMany ? 'hasMany' : (isHasOne ? 'hasOne' : 'scalar'))
});
} else {
fields.push({
name: fieldName,
type: fieldType.trim(),
isRelationship: false
});
}
}
}
return { fields, relationshipFields };
}
};