Skip to content

sync-data-models

Synchronize data models between your React Native app and backend.

Overview

The sync-data-models command creates synchronized data models that work seamlessly between your React Native app and backend, ensuring type safety and consistency across platforms.

Usage

bash
/template sync-data-models <model-names> [options]

Parameters

  • <model-names> - Comma-separated list of models to sync (e.g., User,Post,Comment)

Options

  • --backend - Backend type: supabase, firebase, custom
  • --realtime - Enable real-time synchronization
  • --validation - Add runtime validation with Zod
  • --migrations - Generate migration files

Examples

Basic Model Sync

bash
/template sync-data-models User,Post,Comment --backend supabase

With Real-time Updates

bash
/template sync-data-models Message,Channel --backend firebase --realtime

Full Schema Sync

bash
/template sync-data-models all --validation --migrations

What It Creates

Model Structure

src/
├── models/
│   ├── shared/           # Shared between app and backend
│   │   ├── User.ts
│   │   ├── Post.ts
│   │   └── Comment.ts
│   ├── schemas/          # Validation schemas
│   │   ├── User.schema.ts
│   │   └── index.ts
│   ├── sync/            # Sync configuration
│   │   ├── config.ts
│   │   ├── realtime.ts
│   │   └── transforms.ts
│   └── index.ts

Shared Model Definition

typescript
// src/models/shared/User.ts
import { z } from 'zod';

// Zod schema for validation
export const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  username: z.string().min(3).max(30),
  displayName: z.string().min(1).max(100),
  avatarUrl: z.string().url().optional(),
  bio: z.string().max(500).optional(),
  isVerified: z.boolean().default(false),
  role: z.enum(['user', 'admin', 'moderator']).default('user'),
  preferences: z.object({
    notifications: z.boolean().default(true),
    theme: z.enum(['light', 'dark', 'system']).default('system'),
    language: z.string().default('en'),
  }).default({}),
  createdAt: z.date(),
  updatedAt: z.date(),
  deletedAt: z.date().optional(),
});

// TypeScript type
export type User = z.infer<typeof UserSchema>;

// Database model interface
export interface UserModel extends User {
  // Relations
  posts?: PostModel[];
  comments?: CommentModel[];
  followers?: UserModel[];
  following?: UserModel[];
  
  // Computed properties
  get fullName(): string;
  get initials(): string;
  get isOnline(): boolean;
}

// Model class implementation
export class UserModelImpl implements UserModel {
  constructor(private data: User) {}
  
  get fullName() {
    return this.data.displayName || this.data.username;
  }
  
  get initials() {
    return this.fullName
      .split(' ')
      .map(n => n[0])
      .join('')
      .toUpperCase();
  }
  
  get isOnline() {
    // Implementation depends on presence system
    return false;
  }
  
  // Spread all User properties
  ...this.data
}

WatermelonDB Integration

typescript
// src/models/watermelon/User.ts
import { Model } from '@nozbe/watermelondb';
import { field, date, relation, action } from '@nozbe/watermelondb/decorators';
import { UserSchema, type User } from '../shared/User';

export default class UserWatermelon extends Model implements User {
  static table = 'users';
  static associations = {
    posts: { type: 'has_many', foreignKey: 'user_id' },
    comments: { type: 'has_many', foreignKey: 'user_id' },
  };
  
  @field('email') email!: string;
  @field('username') username!: string;
  @field('display_name') displayName!: string;
  @field('avatar_url') avatarUrl?: string;
  @field('bio') bio?: string;
  @field('is_verified') isVerified!: boolean;
  @field('role') role!: 'user' | 'admin' | 'moderator';
  @field('preferences') preferences!: string; // JSON string
  @date('created_at') createdAt!: Date;
  @date('updated_at') updatedAt!: Date;
  @date('deleted_at') deletedAt?: Date;
  
  // Parse JSON preferences
  get parsedPreferences() {
    return JSON.parse(this.preferences || '{}');
  }
  
  @action async updateFromServer(data: User) {
    const validated = UserSchema.parse(data);
    
    await this.update(() => {
      Object.assign(this, {
        ...validated,
        preferences: JSON.stringify(validated.preferences),
      });
    });
  }
  
  // Convert to shared model
  toSharedModel(): User {
    return UserSchema.parse({
      id: this.id,
      email: this.email,
      username: this.username,
      displayName: this.displayName,
      avatarUrl: this.avatarUrl,
      bio: this.bio,
      isVerified: this.isVerified,
      role: this.role,
      preferences: this.parsedPreferences,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt,
      deletedAt: this.deletedAt,
    });
  }
}

Supabase Integration

typescript
// src/models/supabase/sync.ts
import { createClient } from '@supabase/supabase-js';
import { Database } from './database.types';

const supabase = createClient<Database>(SUPABASE_URL, SUPABASE_KEY);

export class SupabaseSync {
  // Fetch models with type safety
  async fetchUsers(since?: Date) {
    const query = supabase
      .from('users')
      .select(`
        *,
        posts (
          id,
          title,
          created_at
        )
      `);
    
    if (since) {
      query.gte('updated_at', since.toISOString());
    }
    
    const { data, error } = await query;
    
    if (error) throw error;
    return data.map(u => UserSchema.parse(u));
  }
  
  // Real-time subscriptions
  subscribeToUsers(callback: (user: User) => void) {
    return supabase
      .channel('users')
      .on('postgres_changes', {
        event: '*',
        schema: 'public',
        table: 'users',
      }, (payload) => {
        const user = UserSchema.parse(payload.new);
        callback(user);
      })
      .subscribe();
  }
  
  // Sync local changes
  async pushUser(user: User) {
    const { error } = await supabase
      .from('users')
      .upsert(user)
      .eq('id', user.id);
    
    if (error) throw error;
  }
}

Firebase Integration

typescript
// src/models/firebase/sync.ts
import firestore from '@react-native-firebase/firestore';
import { UserSchema, type User } from '../shared/User';

export class FirebaseSync {
  private usersCollection = firestore().collection('users');
  
  // Fetch with validation
  async fetchUser(id: string): Promise<User> {
    const doc = await this.usersCollection.doc(id).get();
    
    if (!doc.exists) {
      throw new Error('User not found');
    }
    
    return UserSchema.parse({
      id: doc.id,
      ...doc.data(),
    });
  }
  
  // Real-time sync
  subscribeToUser(id: string, callback: (user: User) => void) {
    return this.usersCollection.doc(id).onSnapshot((doc) => {
      if (doc.exists) {
        const user = UserSchema.parse({
          id: doc.id,
          ...doc.data(),
        });
        callback(user);
      }
    });
  }
  
  // Batch sync
  async syncUsers(users: User[]) {
    const batch = firestore().batch();
    
    users.forEach((user) => {
      const ref = this.usersCollection.doc(user.id);
      batch.set(ref, user, { merge: true });
    });
    
    await batch.commit();
  }
}

Model Transformations

typescript
// src/models/sync/transforms.ts
export class ModelTransformer {
  // Transform between database and app models
  static toDatabase<T>(model: T, schema: z.ZodSchema<T>): any {
    const validated = schema.parse(model);
    
    // Convert dates to timestamps
    return Object.entries(validated).reduce((acc, [key, value]) => {
      if (value instanceof Date) {
        acc[key] = value.toISOString();
      } else if (typeof value === 'object' && value !== null) {
        acc[key] = JSON.stringify(value);
      } else {
        acc[key] = value;
      }
      return acc;
    }, {} as any);
  }
  
  static fromDatabase<T>(data: any, schema: z.ZodSchema<T>): T {
    // Parse JSON fields
    const parsed = Object.entries(data).reduce((acc, [key, value]) => {
      if (typeof value === 'string' && value.startsWith('{')) {
        try {
          acc[key] = JSON.parse(value);
        } catch {
          acc[key] = value;
        }
      } else {
        acc[key] = value;
      }
      return acc;
    }, {} as any);
    
    return schema.parse(parsed);
  }
}

Migration Generation

typescript
// src/models/migrations/001_create_users.ts
export const migration_001_create_users = {
  version: 1,
  up: async (db: Database) => {
    await db.write(async () => {
      await db.unsafeExecute(`
        CREATE TABLE users (
          id TEXT PRIMARY KEY,
          email TEXT NOT NULL UNIQUE,
          username TEXT NOT NULL UNIQUE,
          display_name TEXT NOT NULL,
          avatar_url TEXT,
          bio TEXT,
          is_verified INTEGER DEFAULT 0,
          role TEXT DEFAULT 'user',
          preferences TEXT DEFAULT '{}',
          created_at INTEGER NOT NULL,
          updated_at INTEGER NOT NULL,
          deleted_at INTEGER
        );
        
        CREATE INDEX idx_users_email ON users (email);
        CREATE INDEX idx_users_username ON users (username);
      `);
    });
  },
  
  down: async (db: Database) => {
    await db.write(async () => {
      await db.unsafeExecute('DROP TABLE users');
    });
  },
};

Real-time Sync Hook

typescript
// src/hooks/useRealtimeSync.ts
export function useRealtimeSync<T>(
  model: string,
  id: string,
  schema: z.ZodSchema<T>
) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  
  useEffect(() => {
    const sync = getSyncAdapter();
    
    // Initial fetch
    sync.fetch(model, id)
      .then(raw => schema.parse(raw))
      .then(setData)
      .catch(setError)
      .finally(() => setLoading(false));
    
    // Subscribe to changes
    const unsubscribe = sync.subscribe(model, id, (raw) => {
      try {
        const parsed = schema.parse(raw);
        setData(parsed);
      } catch (err) {
        setError(err as Error);
      }
    });
    
    return unsubscribe;
  }, [model, id, schema]);
  
  return { data, loading, error };
}

// Usage
const { data: user, loading } = useRealtimeSync('users', userId, UserSchema);

Validation & Type Safety

Runtime Validation

typescript
// Validate before sync
export async function syncModel<T>(
  model: T,
  schema: z.ZodSchema<T>,
  syncFn: (data: T) => Promise<void>
) {
  try {
    const validated = schema.parse(model);
    await syncFn(validated);
  } catch (error) {
    if (error instanceof z.ZodError) {
      console.error('Validation failed:', error.errors);
      throw new ValidationError(error.errors);
    }
    throw error;
  }
}

Type Guards

typescript
export function isUser(obj: any): obj is User {
  return UserSchema.safeParse(obj).success;
}

export function assertUser(obj: any): asserts obj is User {
  UserSchema.parse(obj);
}

Testing

Model Tests

typescript
describe('User Model', () => {
  it('validates correct data', () => {
    const validUser = {
      id: '123e4567-e89b-12d3-a456-426614174000',
      email: 'test@example.com',
      username: 'testuser',
      displayName: 'Test User',
      isVerified: false,
      role: 'user',
      preferences: {},
      createdAt: new Date(),
      updatedAt: new Date(),
    };
    
    expect(() => UserSchema.parse(validUser)).not.toThrow();
  });
  
  it('rejects invalid email', () => {
    const invalidUser = { ...validUser, email: 'not-an-email' };
    
    expect(() => UserSchema.parse(invalidUser)).toThrow();
  });
});

Best Practices

  1. Single Source of Truth: Define models once, use everywhere
  2. Validate Everything: Use schemas for all external data
  3. Handle Schema Evolution: Plan for migrations
  4. Optimize Sync: Only sync changed fields
  5. Type Safety: Leverage TypeScript throughout

Built with ❤️ for the AI Coding community, by Praney Behl