Skip to main content
Glama
field-manager.js10 kB
/** * Field Manager - Core orchestrator for field operations * Handles field CRUD operations within REST API v2 constraints */ export class FieldManager { constructor(apiClient, fieldRegistry, validator) { this.api = apiClient; this.registry = fieldRegistry; this.validator = validator; this.dependencyTracker = null; // Will be injected this.positionEngine = null; // Will be injected } /** * Add a new field to a form with intelligent defaults * @param {number} formId - Target form ID * @param {string} fieldType - Field type from registry * @param {object} properties - Field configuration * @param {object} position - Positioning configuration * @returns {object} Field creation result with warnings */ async addField(formId, fieldType, properties = {}, position = {}) { // 1. Validate field type against registry const fieldDef = this.registry[fieldType]; if (!fieldDef) { throw new Error(`Unknown field type: ${fieldType}`); } // 2. Fetch current form via REST API const form = await this.api.getForm(formId); // 3. Generate unique integer field ID (max + 1 pattern) const fieldId = this.generateFieldId(form.fields || []); // 4. Create field with type-specific defaults const field = this.createField(fieldId, fieldType, properties, fieldDef); // 5. Generate compound sub-inputs if needed (address.1, name.3, etc.) if (fieldDef.storage?.type === 'compound') { field.inputs = this.generateSubInputs(field, fieldDef); } // 6. Calculate insertion position (page-aware) const insertIndex = this.positionEngine?.calculatePosition( form.fields || [], position, form.pagination ) || form.fields?.length || 0; // 7. Insert field at calculated position if (!form.fields) form.fields = []; form.fields.splice(insertIndex, 0, field); // 8. Update entire form via REST API const updatedForm = await this.api.updateForm(form); // 9. Return result with validation warnings return { success: true, field: field, warnings: this.validator?.getWarnings(field) || [], form_id: formId, position: { index: insertIndex, page: field.pageNumber || 1 } }; } /** * Update existing field with dependency checking */ async updateField(formId, fieldId, updates = {}) { // Fetch form const form = await this.api.getForm(formId); // Find field const fieldIndex = form.fields?.findIndex(f => f.id == fieldId); if (fieldIndex === -1) { throw new Error(`Field ${fieldId} not found in form ${formId}`); } // Check dependencies const dependencies = this.dependencyTracker?.scanFormDependencies(form, fieldId) || {}; // Apply updates const originalField = { ...form.fields[fieldIndex] }; form.fields[fieldIndex] = { ...originalField, ...updates, id: originalField.id // Preserve ID }; // Update form const result = await this.api.updateForm(form); return { success: true, field: result.form.fields[fieldIndex], changes: { before: originalField, after: result.form.fields[fieldIndex] }, warnings: { dependencies: dependencies.conditionalLogic?.length > 0 ? ['Field has conditional logic dependencies'] : [], validationIssues: this.validator?.getWarnings(result.form.fields[fieldIndex]) || [] } }; } /** * Delete field with comprehensive dependency analysis */ async deleteField(formId, fieldId, options = {}) { const { cascade = false, force = false } = options; // Fetch form const form = await this.api.getForm(formId); // Check field exists const field = form.fields?.find(f => f.id == fieldId); if (!field) { throw new Error(`Field ${fieldId} not found in form ${formId}`); } // Scan dependencies const dependencies = this.dependencyTracker?.scanFormDependencies(form, fieldId) || {}; const hasBreakingDeps = this.dependencyTracker?.hasBreakingDependencies(dependencies); // Handle dependencies if (hasBreakingDeps && !force) { return { success: false, error: 'Field has dependencies that would break', deleted_field: { id: field.id, type: field.type, label: field.label }, dependencies, suggestion: 'Use force=true to delete anyway, or cascade=true to clean up dependencies' }; } // Remove field form.fields = form.fields.filter(f => f.id != fieldId); // Clean up dependencies if cascade if (cascade && hasBreakingDeps) { this.cleanupDependencies(form, fieldId); } // Update form await this.api.updateForm(form); return { success: true, deleted_field: { id: field.id, type: field.type, label: field.label }, dependencies, actions_taken: cascade ? ['Dependencies cleaned up'] : [] }; } /** * Generate unique integer field ID using max+1 pattern */ generateFieldId(existingFields) { if (!existingFields || existingFields.length === 0) return 1; const maxId = existingFields.reduce((max, field) => { const id = parseInt(field.id); return isNaN(id) ? max : Math.max(max, id); }, 0); return maxId + 1; } /** * Create field with intelligent defaults from registry */ createField(id, type, properties, fieldDef) { return { id, type, label: properties.label || fieldDef.label || 'Untitled', adminLabel: properties.adminLabel || '', isRequired: properties.isRequired || false, size: properties.size || fieldDef.defaults?.size || 'medium', errorMessage: properties.errorMessage || '', visibility: properties.visibility || 'visible', cssClass: properties.cssClass || '', ...this.getTypeSpecificDefaults(type, fieldDef), ...properties }; } /** * Generate compound sub-inputs (address.1, name.3, etc.) */ generateSubInputs(field, fieldDef) { const subInputs = []; const baseId = field.id; // Address field sub-inputs if (field.type === 'address') { const variant = field.addressType || 'us'; if (variant === 'us' || variant === 'international') { subInputs.push( { id: `${baseId}.1`, label: 'Street Address', name: '' }, { id: `${baseId}.2`, label: 'Address Line 2', name: '' }, { id: `${baseId}.3`, label: 'City', name: '' }, { id: `${baseId}.4`, label: variant === 'us' ? 'State' : 'State / Province', name: '' }, { id: `${baseId}.5`, label: variant === 'us' ? 'ZIP Code' : 'ZIP / Postal Code', name: '' }, { id: `${baseId}.6`, label: 'Country', name: '' } ); } else if (variant === 'canadian') { subInputs.push( { id: `${baseId}.1`, label: 'Street Address', name: '' }, { id: `${baseId}.2`, label: 'Address Line 2', name: '' }, { id: `${baseId}.3`, label: 'City', name: '' }, { id: `${baseId}.4`, label: 'Province', name: '' }, { id: `${baseId}.5`, label: 'Postal Code', name: '' }, { id: `${baseId}.6`, label: 'Country', name: '' } ); } } // Name field sub-inputs else if (field.type === 'name') { const format = field.nameFormat || 'advanced'; if (format === 'advanced') { subInputs.push( { id: `${baseId}.2`, label: 'Prefix', name: '' }, { id: `${baseId}.3`, label: 'First', name: '' }, { id: `${baseId}.4`, label: 'Middle', name: '' }, { id: `${baseId}.6`, label: 'Last', name: '' }, { id: `${baseId}.8`, label: 'Suffix', name: '' } ); } else { subInputs.push( { id: `${baseId}.3`, label: 'First', name: '' }, { id: `${baseId}.6`, label: 'Last', name: '' } ); } } // Credit card field sub-inputs else if (field.type === 'creditcard') { subInputs.push( { id: `${baseId}.1`, label: 'Card Number', name: '' }, { id: `${baseId}.2`, label: 'Expiration Date', name: '' }, { id: `${baseId}.3`, label: 'Security Code', name: '' }, { id: `${baseId}.4`, label: 'Cardholder Name', name: '' }, { id: `${baseId}.5`, label: 'Card Type', name: '' } ); } return subInputs; } /** * Get type-specific default values */ getTypeSpecificDefaults(type, fieldDef) { const defaults = {}; // Add choices for choice-based fields if (fieldDef.hasChoices) { defaults.choices = [ { text: 'First Choice', value: 'First Choice' }, { text: 'Second Choice', value: 'Second Choice' }, { text: 'Third Choice', value: 'Third Choice' } ]; } // Add date format for date fields if (type === 'date') { defaults.dateFormat = 'mdy'; defaults.dateType = 'datepicker'; } // Add time format for time fields if (type === 'time') { defaults.timeFormat = '12'; } return defaults; } /** * Clean up dependencies when cascade deleting */ cleanupDependencies(form, fieldId) { // Remove from conditional logic rules form.fields?.forEach(field => { if (field.conditionalLogic?.rules) { field.conditionalLogic.rules = field.conditionalLogic.rules.filter( rule => rule.fieldId != fieldId ); // Disable conditional logic if no rules remain if (field.conditionalLogic.rules.length === 0) { field.conditionalLogic.enabled = false; } } }); // Note: Calculations and merge tags would need manual review // as they use string-based formulas that are harder to clean automatically } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/GravityKit/gravity-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server