All files / backend/src/services/scaffold-model field-inferrer.ts

97.82% Statements 45/46
96.61% Branches 57/59
100% Functions 3/3
97.82% Lines 45/46

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123                        55x 2x     53x 6x     47x 7x     40x   18x 3x   15x     22x 13x 2x   11x 11x 3x   8x     9x 9x                                   22x 38x 38x           38x 8x 8x       38x 1x 1x 1x         38x 7x 7x 6x 6x 1x 5x 1x       7x           38x     22x                   9x 1x   8x 3x   5x    
/**
 * Field Inferrer
 *
 * Infers field types from data values for scaffold model generation.
 */
 
import type { InferredField } from './types.js';
 
/**
 * Infer field type from a JavaScript value
 */
export function inferFieldType(key: string, value: unknown): string {
  if (value === null || value === undefined) {
    return 'string';
  }
 
  if (typeof value === 'boolean') {
    return 'boolean';
  }
 
  if (typeof value === 'number') {
    return 'number';
  }
 
  if (typeof value === 'string') {
    // Special case: mainContent field should be markdown
    if (key === 'mainContent' || key === 'body' || key === 'content') {
      return 'markdown';
    }
    return 'string';
  }
 
  if (Array.isArray(value)) {
    if (value.length === 0) {
      return 'leaf-array';
    }
    const firstItem = value[0];
    if (typeof firstItem === 'object' && firstItem !== null) {
      return 'accordion';
    }
    return 'leaf-array';
  }
 
  Eif (typeof value === 'object') {
    return 'nest';
  }
 
  return 'string';
}
 
/**
 * Recursively parse object keys into field definitions
 *
 * @param obj - The object to parse
 * @param fields - The array to populate with inferred fields
 * @param level - Current nesting level (for debugging)
 */
export function parseKeysToFields(
  obj: Record<string, unknown>,
  fields: InferredField[] = [],
  level: number = 0
): InferredField[] {
  for (const [key, value] of Object.entries(obj)) {
    const fieldType = inferFieldType(key, value);
    const field: InferredField = {
      key,
      type: fieldType,
    };
 
    // Handle nested objects
    if (fieldType === 'nest' && typeof value === 'object' && value !== null) {
      field.groupdata = true;
      field.fields = parseKeysToFields(value as Record<string, unknown>, [], level + 1);
    }
 
    // Handle arrays of objects (accordion)
    if (fieldType === 'accordion' && Array.isArray(value) && value.length > 0) {
      const firstItem = value[0];
      Eif (typeof firstItem === 'object' && firstItem !== null) {
        field.fields = parseKeysToFields(firstItem as Record<string, unknown>, [], level + 1);
      }
    }
 
    // Handle leaf-array: add child field definition
    if (fieldType === 'leaf-array') {
      let childType = 'string'; // default
      if (Array.isArray(value) && value.length > 0) {
        const firstItem = value[0];
        if (typeof firstItem === 'boolean') {
          childType = 'boolean';
        } else if (typeof firstItem === 'number') {
          childType = 'number';
        }
        // strings remain 'string'
      }
      field.field = {
        key: 'item',
        type: childType,
      };
    }
 
    fields.push(field);
  }
 
  return fields;
}
 
/**
 * Infer fields from parsed content data
 *
 * @param data - The parsed content data (frontmatter or raw data file)
 * @returns Array of inferred field definitions
 */
export function inferFieldsFromData(data: unknown): InferredField[] {
  if (data === null || data === undefined) {
    return [];
  }
  if (typeof data !== 'object' || Array.isArray(data)) {
    return [];
  }
  return parseKeysToFields(data as Record<string, unknown>);
}