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 supabaseWith Real-time Updates
bash
/template sync-data-models Message,Channel --backend firebase --realtimeFull Schema Sync
bash
/template sync-data-models all --validation --migrationsWhat 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.tsShared 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
- Single Source of Truth: Define models once, use everywhere
- Validate Everything: Use schemas for all external data
- Handle Schema Evolution: Plan for migrations
- Optimize Sync: Only sync changed fields
- Type Safety: Leverage TypeScript throughout
Related Commands
add-offline-sync- Enable offline syncadd-api-client- Create typed API clientsetup-shared-backend- Configure backend sync
