Migration Guide V1 To V2

Ecosistema completo para organización y gestión de contenido con interfaces espaciales

📦 Migration Guide: Property System V1 → V2

Purpose: Step-by-step guide to migrate from inline property definitions to property references


🎯 What We're Migrating

Before (V1 - Inline Definitions)

export const BUTTON_EMBEDDABLE = {
  id: 'ui-button',
  inputs: [
    {
      name: 'text',
      type: 'string',
      required: false,
      description: 'Button label text',
      defaultValue: 'Button',
      group: 'content',
      order: 1,
    },
    {
      name: 'variant',
      type: 'string',
      required: false,
      description: 'Button variant: primary | secondary | ghost | danger',
      defaultValue: 'primary',
      group: 'style',
      order: 1,
      transform: mapButtonVariant,
      options: [
        { value: 'primary', label: 'Primary (Solid)' },
        { value: 'secondary', label: 'Secondary (Outline)' },
        { value: 'ghost', label: 'Ghost (Text Only)' },
        { value: 'danger', label: 'Danger (Destructive)' },
      ],
    },
    // ... 10 more properties (200+ lines total)
  ],
};

Issues:

After (V2 - Property References)

export const BUTTON_EMBEDDABLE_V2 = {
  id: 'ui-button-v2',
  propertyInputs: [
    {
      propertyId: 'core_label',          // ✅ Backend reference
      bindTo: 'text',
      override: { order: 1, group: 'content' },
    },
    {
      propertyId: 'custom_buttonVariant',
      bindTo: 'variant',
      override: { order: 1, group: 'style', transform: 'mapButtonVariant' },
    },
    // ... 8 more references (~30 lines total)
  ],
};

Benefits:


📋 Migration Checklist

Phase 1: Backend Property Creation

For each inline property, create or find equivalent in backend:

1. Identify Core vs Custom Properties

Property Type Example
Core (reusable) core_* core_label, core_disabled, core_size
Custom (widget-specific) custom_* custom_buttonVariant, custom_buttonShape

2. Create Missing Backend Properties

# POST /api/properties
curl -X POST http://localhost:3000/api/properties \
  -H "Content-Type: application/json" \
  -d '{
    "id": "custom_buttonVariant",
    "key": "buttonVariant",
    "label": "Button Variant",
    "description": "Visual style variant for buttons",
    "controlType": "select",
    "category": "appearance",
    "defaultValue": "primary",
    "validation": {
      "options": [
        { "value": "primary", "label": "Primary (Solid)", "description": "Solid filled button" },
        { "value": "secondary", "label": "Secondary (Outline)", "description": "Outlined button" },
        { "value": "ghost", "label": "Ghost (Text Only)", "description": "Text-only button" },
        { "value": "danger", "label": "Danger (Destructive)", "description": "Destructive action button" }
      ]
    },
    "widgetTypes": ["button"],
    "tags": ["button", "appearance", "variant"],
    "aiContext": "Button variants: primary (solid fill), secondary (outline), ghost (text only), danger (destructive). Use primary for main actions, secondary for less important actions.",
    "metadata": {
      "created": "2025-12-10T00:00:00Z",
      "modified": "2025-12-10T00:00:00Z",
      "version": "1.0.0",
      "usageCount": 0,
      "isBuiltIn": false,
      "author": "system"
    }
  }'

3. Verify Property Exists

# GET /api/properties/:id
curl http://localhost:3000/api/properties/custom_buttonVariant

Phase 2: Update Embeddable Definition

1. Create V2 Version (keep V1 for comparison)

// button.embeddable.v2.ts
import type { PropertyReference } from '@libs-foundational/core-properties';

export const BUTTON_EMBEDDABLE_V2 = {
  id: 'ui-button-v2',
  displayName: 'UI Button (V2)',
  description: 'Button with property references',
  category: 'form',
  componentType: undefined,
  permissions: [],
  
  // NEW: Property references
  propertyInputs: [
    // ... property references here
  ],
  
  // Keep for backward compatibility (auto-generated)
  inputs: [],
  
  outputs: [/* ... */],
};

2. Map Each Property

For each inline property in V1:

V1 Field V2 Mapping Example
name bindTo 'text'
type, description, defaultValue (Backend) Removed
transform function override.transform (name as string) 'mapButtonVariant'
group, order override.group, override.order { group: 'style', order: 1 }
options (Backend) Removed

Example Mapping:

// V1
{
  name: 'variant',
  type: 'string',
  description: 'Button variant',
  defaultValue: 'primary',
  group: 'style',
  order: 1,
  transform: mapButtonVariant,
  options: [...]
}

// V2
{
  propertyId: 'custom_buttonVariant', // ✅ Find/create in backend
  bindTo: 'variant',                  // ✅ Same as V1 'name'
  override: {
    group: 'style',                   // ✅ Same as V1
    order: 1,                          // ✅ Same as V1
    transform: 'mapButtonVariant',    // ✅ Function name as string
  }
}

Phase 3: Update Variants

1. Convert inputOverrides to propertyValues

// V1
export const BUTTON_EMBEDDABLE_VARIANTS = [
  {
    id: 'primary-md',
    name: 'Primary Medium',
    inputOverrides: {
      variant: 'primary',
      size: 'md',
      shape: 'rounded',
    },
  },
];

// V2
export const BUTTON_EMBEDDABLE_V2_VARIANTS = [
  {
    id: 'primary-md',
    embeddableId: 'ui-button-v2',     // ✅ Add embeddable reference
    name: 'Primary Medium',
    propertyValues: {                  // ✅ Rename: inputOverrides → propertyValues
      'custom_buttonVariant': 'primary', // ✅ Use propertyId as key
      'core_size': 'md',
      'custom_buttonShape': 'rounded',
    },
    metadata: {                        // ✅ Add metadata
      created: new Date().toISOString(),
      createdBy: 'system',
      tags: ['primary', 'default'],
      usageCount: 0,
    },
  },
];

Phase 4: Test Migration

1. Verify Property Resolution

import { PropertyResolverService } from '@libs-foundational/core-properties';

const resolver = inject(PropertyResolverService);

// Test resolving embeddable properties
const resolved = await resolver.resolveEmbeddableProperties('ui-button-v2');

// Should return Map of bindTo → ResolvedProperty
console.log(resolved.get('text')); // { id: 'core_label', key: 'label', ... }
console.log(resolved.get('variant')); // { id: 'custom_buttonVariant', ... }

2. Component Integration Test

// In component
export class UiButtonComponent {
  // Properties resolved at runtime
  @Input() properties: Map<string, ResolvedProperty>;
  
  // Or traditional inputs (populated by resolver)
  @Input() text = '';
  @Input() variant = 'primary';
  
  ngOnInit() {
    // Auto-populate from resolved properties
    if (this.properties) {
      this.text = this.properties.get('text')?.value as string;
      this.variant = this.properties.get('variant')?.value as string;
    }
  }
}

🔧 Tools & Scripts

Auto-Migration Script

/**
 * Auto-migrate V1 embeddable to V2
 * 
 * Usage:
 *   npm run migrate-embeddable -- button
 */
import { readFileSync, writeFileSync } from 'fs';

function migrateEmbeddable(embeddableName: string) {
  const v1Path = `embeddables/embeddable-schemas/src/lib/ui-core/${embeddableName}.embeddable.ts`;
  const v2Path = `embeddables/embeddable-schemas/src/lib/ui-core/${embeddableName}.embeddable.v2.ts`;
  
  const v1Content = readFileSync(v1Path, 'utf-8');
  
  // Parse V1 embeddable
  const v1Embeddable = parseV1Embeddable(v1Content);
  
  // Convert to V2
  const v2Embeddable = {
    ...v1Embeddable,
    propertyInputs: v1Embeddable.inputs.map(input => ({
      propertyId: inferPropertyId(input),
      bindTo: input.name,
      override: {
        group: input.group,
        order: input.order,
        transform: input.transform?.name,
      }
    }))
  };
  
  // Write V2 file
  writeFileSync(v2Path, generateV2Code(v2Embeddable));
  
  console.log(`✅ Migrated ${embeddableName} to V2`);
}

function inferPropertyId(input: any): string {
  // Common properties → core_*
  if (['text', 'label', 'disabled', 'visible', 'size'].includes(input.name)) {
    return `core_${input.name}`;
  }
  
  // Widget-specific → custom_*
  return `custom_${input.name}`;
}

Property Sync Script

#!/bin/bash
# Sync backend properties to frontend types

echo "Fetching backend properties..."
curl http://localhost:3000/api/properties > /tmp/backend-properties.json

echo "Generating TypeScript definitions..."
node scripts/generate-property-types.js /tmp/backend-properties.json

echo "✅ Property types synced"

🎯 Migration Priority

Phase 1: Core UI Components (HIGH)

Phase 2: Forms (MEDIUM)

Phase 3: Feedback (MEDIUM)

Phase 4: Complex (LOW)


🚨 Common Issues & Solutions

Issue 1: Property Not Found

Error: PropertyResolverService: Property 'custom_buttonVariant' not found

Solution:

# Check if property exists in backend
curl http://localhost:3000/api/properties/custom_buttonVariant

# If 404, create it
curl -X POST http://localhost:3000/api/properties -d '{...}'

Issue 2: Mapper Function Not Found

Error: PropertyResolverService: Mapper 'mapButtonVariant' not found

Solution:

// In PropertyResolverService, register mapper
private mappers = {
  mapButtonVariant,
  mapSize,
  // ... add missing mapper
};

Issue 3: Component Input Mismatch

Error: Property binding 'variant' doesn't match component input

Solution:

// Ensure bindTo matches @Input() name
export class UiButtonComponent {
  @Input() variant = 'primary'; // ✅ Matches bindTo: 'variant'
}

📚 Related Documentation


Status: 🟡 Guide Complete (Implementation Pending)
Last Updated: December 10, 2025