CRUD Operations
GenericPowerSyncService provides a reusable service class for CRUD operations on any PowerSync table.
Service Initialization
Section titled “Service Initialization”import ZyraForm
let service = GenericPowerSyncService( tableName: "\(AppConfig.dbPrefix)users", userId: currentUserId)Read Operations
Section titled “Read Operations”Load All Records
Section titled “Load All Records”let config = UsersSchema.toTableFieldConfig()
try await service.loadRecords( fields: config.allFields, encryptedFields: config.encryptedFields, integerFields: config.integerFields, booleanFields: config.booleanFields, orderBy: config.defaultOrderBy)
// Access recordslet users = service.recordsLoad with WHERE Clause
Section titled “Load with WHERE Clause”try await service.loadRecords( fields: config.allFields, whereClause: "is_active = ? AND age > ?", parameters: [true, 18], encryptedFields: config.encryptedFields, orderBy: "name ASC")Load Single Record
Section titled “Load Single Record”try await service.loadRecords( fields: config.allFields, whereClause: "id = ?", parameters: [recordId], encryptedFields: config.encryptedFields)
guard let user = service.records.first else { throw NSError(domain: "Service", code: 404)}Create Operations
Section titled “Create Operations”Create Single Record
Section titled “Create Single Record”let id = try await service.createRecord( fields: [ "email": "user@example.com", "name": "John Doe", "age": 25 ], encryptedFields: config.encryptedFields, autoGenerateId: true, autoTimestamp: true)
print("Created record with ID: \(id)")Create with Custom ID
Section titled “Create with Custom ID”let id = try await service.createRecord( fields: [ "id": customId, "email": "user@example.com", "name": "John Doe" ], encryptedFields: config.encryptedFields, autoGenerateId: false)Create Multiple Records
Section titled “Create Multiple Records”let ids = try await service.createRecords( records: [ ["email": "user1@example.com", "name": "User 1"], ["email": "user2@example.com", "name": "User 2"], ["email": "user3@example.com", "name": "User 3"] ], encryptedFields: config.encryptedFields)
print("Created \(ids.count) records")Update Operations
Section titled “Update Operations”Update Single Record
Section titled “Update Single Record”try await service.updateRecord( id: recordId, fields: [ "name": "Updated Name", "age": 26 ], encryptedFields: config.encryptedFields, autoTimestamp: true)Update Without Timestamp
Section titled “Update Without Timestamp”try await service.updateRecord( id: recordId, fields: ["name": "New Name"], encryptedFields: config.encryptedFields, autoTimestamp: false)Delete Operations
Section titled “Delete Operations”Delete Single Record
Section titled “Delete Single Record”try await service.deleteRecord(id: recordId)Delete with Case-Insensitive Matching
Section titled “Delete with Case-Insensitive Matching”try await service.deleteRecord(id: recordId, caseInsensitive: true)Delete Multiple Records
Section titled “Delete Multiple Records”try await service.deleteRecords(ids: [id1, id2, id3])Complete Example
Section titled “Complete Example”struct UserListView: View { @StateObject private var service: GenericPowerSyncService @State private var users: [[String: Any]] = [] @State private var selectedUserId: String?
private let config = UsersSchema.toTableFieldConfig()
init(userId: String) { _service = StateObject(wrappedValue: GenericPowerSyncService( tableName: "\(AppConfig.dbPrefix)users", userId: userId )) }
var body: some View { List(users, id: \.self) { user in HStack { Text(user["name"] as? String ?? "Unknown") .foregroundColor(selectedUserId == user["id"] as? String ? .blue : .primary) Spacer() Text(user["email"] as? String ?? "") .font(.caption) .foregroundColor(.secondary) } .onTapGesture { selectedUserId = user["id"] as? String } } .toolbar { ToolbarItem(placement: .primaryAction) { Button("Add") { Task { await createUser() } } }
ToolbarItem(placement: .secondaryAction) { Button("Delete") { if let id = selectedUserId { Task { await deleteUser(id: id) } } } .disabled(selectedUserId == nil) } } .task { await loadUsers() } }
private func loadUsers() async { do { try await service.loadRecords( fields: config.allFields, encryptedFields: config.encryptedFields, integerFields: config.integerFields, orderBy: config.defaultOrderBy )
await MainActor.run { users = service.records } } catch { print("Failed to load users: \(error)") } }
private func createUser() async { do { let id = try await service.createRecord( fields: [ "email": "newuser@example.com", "name": "New User", "age": 25 ], encryptedFields: config.encryptedFields )
await loadUsers() await MainActor.run { selectedUserId = id } } catch { print("Failed to create user: \(error)") } }
private func updateUser(id: String) async { guard let user = users.first(where: { $0["id"] as? String == id }) else { return }
do { try await service.updateRecord( id: id, fields: [ "name": (user["name"] as? String ?? "") + " (Updated)" ], encryptedFields: config.encryptedFields )
await loadUsers() } catch { print("Failed to update user: \(error)") } }
private func deleteUser(id: String) async { do { try await service.deleteRecord(id: id) await loadUsers()
await MainActor.run { if selectedUserId == id { selectedUserId = nil } } } catch { print("Failed to delete user: \(error)") } }}Field Configuration
Section titled “Field Configuration”Use TableFieldConfig to organize field lists:
struct TableFieldConfig { let allFields: [String] let encryptedFields: [String] let integerFields: [String] let booleanFields: [String] let defaultOrderBy: String
static let users = TableFieldConfig( allFields: ["id", "email", "name", "age", "user_id", "created_at", "updated_at"], encryptedFields: ["name"], integerFields: ["age"], booleanFields: [], defaultOrderBy: "created_at DESC" )}
// Usagelet config = TableFieldConfig.userstry await service.loadRecords( fields: config.allFields, encryptedFields: config.encryptedFields, integerFields: config.integerFields, booleanFields: config.booleanFields, orderBy: config.defaultOrderBy)Best Practices
Section titled “Best Practices”- Always Reload After Mutations: The service automatically reloads, but you can manually call
loadRecords()if needed - Use Field Configurations: Define field lists once with
TableFieldConfig - Handle Errors: Wrap CRUD operations in
do-catchblocks - Type-Safe Access: Use type casting when accessing record fields:
let name = record["name"] as? String ?? ""let age = record["age"] as? Int ?? 0let isActive = record["is_active"] as? Bool ?? false
Next Steps
Section titled “Next Steps”- Form Components - Connect forms to CRUD operations
- PowerSync Integration - Load forms from PowerSync
- Schema Definition - Define your table structure