🗄️ ExtendedTable
Schema definition that extracts PowerSync table definitions, validation rules, metadata, and code generation into a single fluent API.
ZyraForm is built on three core components that work together to eliminate boilerplate code and provide type-safe, schema-driven development. Understanding what each component extracts and why it’s helpful will help you leverage ZyraForm’s full power.
🗄️ ExtendedTable
Schema definition that extracts PowerSync table definitions, validation rules, metadata, and code generation into a single fluent API.
📝 PowerSyncForm
Form state management that extracts React Hook Form patterns, validation logic, error handling, and field watching into SwiftUI.
⚙️ GenericPowerSyncService
CRUD operations that extract repetitive service class code into a single reusable service for all tables.
ExtendedTable extracts multiple separate concerns into a single schema definition:
⚠️ ALL OF THIS MUST BE WRITTEN FOR EVERY SINGLE TABLE
// 1. PowerSync Table (separate file - REPEAT FOR EVERY TABLE)let itemsTable = Table( name: "myApp-items", columns: [ .text("item_name"), .text("owner_id"), .integer("age"), .text("is_happy") ])
// 2. TableFieldConfig (separate file - HARDCODED VALUES FOR EVERY TABLE)static let items = TableFieldConfig( allFields: ["id", "item_name", "owner_id", "age", "is_happy", "created_at", "updated_at"], encryptedFields: ["item_name", "age", "is_happy"], integerFields: ["age"], booleanFields: ["is_happy"], defaultOrderBy: "created_at DESC")// ⚠️ Need to manually list EVERY field name// ⚠️ Need to manually remember which fields are encrypted// ⚠️ Need to manually remember which fields are integers/booleans// ⚠️ REPEAT THIS ENTIRE CONFIG FOR EVERY TABLE IN YOUR APP
// 3. Validation Logic (scattered throughout codebase - REPEAT FOR EVERY TABLE)func validateItemName(_ name: String) -> String? { if name.count < 2 { return "Name too short" } if name.count > 50 { return "Name too long" } return nil}// ⚠️ Validation logic duplicated from schema// ⚠️ Easy to forget validation rules// ⚠️ Must be maintained separately
// 4. Swift Model (separate file - REPEAT FOR EVERY TABLE)struct Item: Codable { let id: String let itemName: String let ownerId: String let age: Int? let isHappy: Bool // ... CodingKeys, etc.}// ⚠️ Manual snake_case to camelCase conversion// ⚠️ Must manually keep in sync with schema
// 5. Drizzle Schema (separate TypeScript file - REPEAT FOR EVERY TABLE)export const items = createTable("items", { item_name: text(), owner_id: text(), age: integer(), is_happy: boolean()});// ⚠️ Must manually maintain TypeScript schema// ⚠️ Easy to get out of sync with Swift schema
// 6. SQL Migration (separate SQL file - REPEAT FOR EVERY TABLE)CREATE TABLE "myApp-items" ( id TEXT PRIMARY KEY, item_name TEXT NOT NULL, ...);// ⚠️ Manual SQL generation// ⚠️ Must remember foreign keys, enums, triggersProblems:
// ONE schema definition replaces all 6 files abovelet ItemsSchema = ExtendedTable( name: "\(AppConfig.dbPrefix)items", columns: [ .text("item_name").encrypted().minLength(2).maxLength(50).notNull(), .text("owner_id").uuid().notNull(), .text("age").int().encrypted().positive().nullable(), .text("is_happy").bool().encrypted().default(false) ])
// Automatically generates everything:// ✅ PowerSync table: ItemsSchema.toPowerSyncTable()// ✅ Field config: ItemsSchema.toTableFieldConfig()// ✅ Swift model: ItemsSchema.generateSwiftModel() (for reference)// ✅ Drizzle schema: ItemsSchema.generateDrizzleSchema() (for reference)// ✅ SQL migration: ItemsSchema.generateCreateTableSQL() (for reference)Benefits: ✨
1. Single Source of Truth
2. Less Code
id, created_at, updated_at added automatically3. Type Safety
4. Complete Code Generation
updated_at$onUpdate)PowerSyncForm extracts React Hook Form patterns into SwiftUI:
// Manual ObservableObject boilerplateclass ItemForm: ObservableObject { @Published var itemName: String = "" @Published var ownerId: String = "" @Published var age: Int? @Published var isHappy: Bool = false
@Published var errors: [String: String] = [:] @Published var isValid: Bool = false @Published var isDirty: Bool = false
// Manual validation func validate() { errors.removeAll()
if itemName.isEmpty { errors["itemName"] = "Name is required" } else if itemName.count < 2 { errors["itemName"] = "Name must be at least 2 characters" } else if itemName.count > 50 { errors["itemName"] = "Name must be at most 50 characters" }
if let age = age, age <= 0 { errors["age"] = "Age must be positive" }
// ... more validation logic
isValid = errors.isEmpty }
// Manual field watching func watch(_ field: String) -> Any? { switch field { case "itemName": return itemName case "ownerId": return ownerId case "age": return age case "isHappy": return isHappy default: return nil } }
// Manual submission func submit(handler: @escaping (ItemFormValues) -> Void) { validate() if isValid { handler(ItemFormValues(...)) } }}Problems:
// Form automatically uses schema for validation@StateObject var form = PowerSyncForm<ItemFormValues>(schema: ItemsSchema)
// Automatic validation based on schema rules// Automatic error messages// Automatic field watching// Automatic form state management
// In SwiftUITextField("Item Name", text: form.binding(for: "item_name") ?? .constant(""))if form.hasError("item_name") { Text(form.getError("item_name") ?? "")}
// Submit with automatic validationform.handleSubmit { values in // Values are guaranteed to be valid}Benefits: ✨
1. Schema-Driven Validation
2. React Hook Form Patterns
3. Less Boilerplate
4. Type Safety
5. SwiftUI Integration
GenericPowerSyncService extracts repetitive service class code into a single reusable service:
⚠️ THIS IS THE FULL CRUD SERVICE FOR JUST ONE TABLE - MUST REPEAT FOR EVERY TABLE
// FULL CRUD Service Class - MUST WRITE THIS FOR EVERY SINGLE TABLE@MainActorclass PowerSyncItemService: ObservableObject { @Published var Items: [[String: Any]] = [] private let powerSync = db private let userId: String private let encryptionManager = SecureEncryptionManager.shared
init(userId: String) { self.userId = userId }
// READ - Load all items public func loadAllItems() async throws { let query = "SELECT * FROM \"myApp-items\" WHERE user_id = ? ORDER BY created_at DESC"
var allResults: [[String: Any]] = [] for try await results in try powerSync.watch( sql: query, parameters: [userId], mapper: { cursor in var dict: [String: Any] = [:]
dict["id"] = try cursor.getString(name: "id") dict["owner_id"] = try cursor.getString(name: "owner_id") dict["created_at"] = try cursor.getString(name: "created_at") dict["updated_at"] = try cursor.getString(name: "updated_at")
// Decrypt encrypted fields let encryptedItemName = try cursor.getString(name: "item_name") dict["item_name"] = try? self.encryptionManager.decryptIfEnabled( encryptedItemName, for: self.userId )
let encryptedAge = try cursor.getString(name: "age") if let decryptedAge = try? self.encryptionManager.decryptIfEnabled( encryptedAge, for: self.userId ), let ageInt = Int(decryptedAge) { dict["age"] = ageInt } else { dict["age"] = try? cursor.getIntOptional(name: "age") }
let encryptedHappyBool = try cursor.getString(name: "is_happy") if let decryptedBool = try? self.encryptionManager.decryptIfEnabled( encryptedHappyBool, for: self.userId ) { dict["is_happy"] = decryptedBool == "true" || decryptedBool == "1" } else { dict["is_happy"] = (try? cursor.getIntOptional(name: "is_happy")) == 1 }
return dict } ) { allResults = results break } Items = allResults }
// CREATE - Create new item public func createNewItem( itemName: String, ownerId: String, age: Int, isHappy: Bool? = false ) async throws -> String { let id = UUID().uuidString let now = ISO8601DateFormatter().string(from: Date())
let encryptedItemName = try encryptionManager.encryptIfEnabled(itemName, for: userId) let encryptedAge = try encryptionManager.encryptIfEnabled(String(age), for: userId) let encryptedHappyBool = try encryptionManager.encryptIfEnabled( isHappy == true ? "true" : "false", for: userId )
try await powerSync.execute(sql: """ INSERT INTO "myApp-items" (id, item_name, owner_id, age, is_happy, created_at, updated_at, user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, parameters: [ id, encryptedItemName, ownerId, encryptedAge, encryptedHappyBool, now, now, userId ] )
try await loadAllItems() return id }
// UPDATE - Update existing item public func updateItem( id: String, itemName: String? = nil, ownerId: String? = nil, age: Int? = nil, isHappy: Bool? = nil ) async throws { let now = ISO8601DateFormatter().string(from: Date())
var updateFields: [String] = [] var parameters: [Any] = []
if let itemName = itemName { let encryptedItemName = try encryptionManager.encryptIfEnabled(itemName, for: userId) updateFields.append("item_name = ?") parameters.append(encryptedItemName) }
if let ownerId = ownerId { updateFields.append("owner_id = ?") parameters.append(ownerId) }
if let age = age { let encryptedAge = try encryptionManager.encryptIfEnabled(String(age), for: userId) updateFields.append("age = ?") parameters.append(encryptedAge) }
if let isHappy = isHappy { let encryptedHappyBool = try encryptionManager.encryptIfEnabled( isHappy ? "true" : "false", for: userId ) updateFields.append("is_happy = ?") parameters.append(encryptedHappyBool) }
updateFields.append("updated_at = ?") parameters.append(now) parameters.append(id)
let query = "UPDATE \"myApp-items\" SET \(updateFields.joined(separator: ", ")) WHERE id = ?"
try await powerSync.execute(sql: query, parameters: parameters) try await loadAllItems() }
// DELETE - Delete item public func deleteItem(id: String) async throws { try await powerSync.execute( sql: "DELETE FROM \"myApp-items\" WHERE LOWER(id) = LOWER(?)", parameters: [id] ) try await loadAllItems() }}
// ⚠️ MUST REPEAT THIS ENTIRE CLASS FOR EVERY TABLE:// PowerSyncItemService (200+ lines)// PowerSyncProjectService (200+ lines)// PowerSyncUserService (200+ lines)// PowerSyncTaskService (200+ lines)// ... 10 tables = 2000+ lines of nearly identical codeProblems:
// ONE service works for ALL tables - no need to write custom service classes!let itemsService = GenericPowerSyncService( tableName: "\(AppConfig.dbPrefix)items", userId: currentUserId)
let projectsService = GenericPowerSyncService( tableName: "\(AppConfig.dbPrefix)projects", userId: currentUserId)
// Same methods work for ANY table - automatic encryption, type conversion, SQL buildinglet config = ItemsSchema.toTableFieldConfig() // Generated from schema!
try await itemsService.loadRecords( fields: config.allFields, // ✅ Auto-generated from schema encryptedFields: config.encryptedFields, // ✅ Auto-detected from schema integerFields: config.integerFields, // ✅ Auto-detected from schema booleanFields: config.booleanFields // ✅ Auto-detected from schema)
try await itemsService.createRecord( fields: [ "item_name": "New Item", "age": 25, "is_happy": true ], encryptedFields: config.encryptedFields // ✅ Automatic encryption!)
try await itemsService.updateRecord( id: itemId, fields: ["age": 26], encryptedFields: config.encryptedFields // ✅ Automatic encryption!)
try await itemsService.deleteRecord(id: itemId)
// ✅ Use the SAME service for projects, users, tasks - ANY table!try await projectsService.loadRecords( fields: ProjectsSchema.toTableFieldConfig().allFields, encryptedFields: ProjectsSchema.toTableFieldConfig().encryptedFields)Benefits: ✨
1. Eliminates Repetitive Code
2. No More Manual SQL Strings
3. Automatic Field Management
4. Consistent Patterns
These three components form a complete ecosystem:
// 1. Define schema ONCElet UsersSchema = ExtendedTable( name: "\(AppConfig.dbPrefix)users", columns: [ .text("email").email().notNull(), .text("name").minLength(2).maxLength(50).notNull() ])
// 2. Create form with automatic validation@StateObject var form = PowerSyncForm<UserFormValues>(schema: UsersSchema)
// 3. Create service with automatic CRUD@StateObject var service = GenericPowerSyncService( tableName: UsersSchema.name, userId: currentUserId)
// 4. Everything works together automaticallyform.handleSubmit { values in let config = UsersSchema.toTableFieldConfig() try await service.createRecord( fields: values, encryptedFields: config.encryptedFields )}The Flow:
Result:
| Component | Extracts | Before (PER TABLE) | After | Benefit |
|---|---|---|---|---|
| ExtendedTable 🗄️ | Schema + Validation + Config + Code Gen | 6+ files, 200+ lines Must repeat for EVERY table | 1 definition, ~8 lines | ~96% less code ✅ Write once, use everywhere |
| PowerSyncForm 📝 | Form State + Validation + Errors | 100+ lines boilerplate Must repeat for EVERY form | 1 line initialization | ~99% less code ✅ Schema-driven validation |
| GenericPowerSyncService ⚙️ | CRUD Operations | 200+ lines per table Non-typesafe SQL strings Manual encryption/type conversion Must repeat for EVERY table | 1 service for all tables | ~95% less code ✅ Typesafe, automatic encryption |
Note: Percentages are estimates based on typical form development patterns. Actual code reduction varies by project complexity.
🎯 Total Impact:
✨ Key Insight: Without ZyraForm, you must write hundreds of lines of repetitive, error-prone code for EVERY table. With ZyraForm, you define your schema once and everything else is automatic!
Note: These metrics are estimates based on typical form development patterns. Actual savings may vary depending on form complexity and existing codebase structure.