Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,24 @@ The easiest way to test the PowerSync Swift SDK is to run our demo application.
Add

```swift
.package(url: "https://github.com/powersync-ja/powersync-swift", exact: "<version>")
dependencies: [
...
.package(url: "https://github.com/powersync-ja/powersync-swift", exact: "<version>")
],
targets: [
.target(
name: "YourTargetName",
dependencies: [
...
.product(
name: "PowerSync",
package: "powersync-swift"
),
]
)
]
```


to your `Package.swift` file and pin the dependency to a specific version. This is required because the package is in beta.

## Underlying Kotlin Dependency
Expand Down
10 changes: 5 additions & 5 deletions Sources/PowerSync/Kotlin/KotlinAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ internal struct KotlinAdapter {
)
}
}

struct IndexedColumn {
static func toKotlin(_ column: IndexedColumnProtocol) -> PowerSyncKotlin.IndexedColumn {
return PowerSyncKotlin.IndexedColumn(
Expand All @@ -20,7 +20,7 @@ internal struct KotlinAdapter {
)
}
}

struct Table {
static func toKotlin(_ table: TableProtocol) -> PowerSyncKotlin.Table {
PowerSyncKotlin.Table(
Expand All @@ -33,15 +33,15 @@ internal struct KotlinAdapter {
)
}
}

struct Column {
static func toKotlin(_ column: any ColumnProtocol) -> PowerSyncKotlin.Column {
PowerSyncKotlin.Column(
name: column.name,
type: columnType(from: column.type)
)
}

private static func columnType(from swiftType: ColumnData) -> PowerSyncKotlin.ColumnType {
switch swiftType {
case .text:
Expand All @@ -53,7 +53,7 @@ internal struct KotlinAdapter {
}
}
}

struct Schema {
static func toKotlin(_ schema: SchemaProtocol) -> PowerSyncKotlin.Schema {
PowerSyncKotlin.Schema(
Expand Down
34 changes: 29 additions & 5 deletions Sources/PowerSync/PowerSyncBackendConnector.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,40 @@
public protocol PowerSyncBackendConnectorProtocol {
func uploadData(database: PowerSyncDatabaseProtocol) async throws

///
/// Get credentials for PowerSync.
///
/// This should always fetch a fresh set of credentials - don't use cached
/// values.
///
/// Return null if the user is not signed in. Throw an error if credentials
/// cannot be fetched due to a network error or other temporary error.
///
/// This token is kept for the duration of a sync connection.
///
func fetchCredentials() async throws -> PowerSyncCredentials?

///
/// Upload local changes to the app backend.
///
/// Use [getCrudBatch] to get a batch of changes to upload.
///
/// Any thrown errors will result in a retry after the configured wait period (default: 5 seconds).
///
func uploadData(database: PowerSyncDatabaseProtocol) async throws
}

/// Implement this to connect an app backend.
///
/// The connector is responsible for:
/// 1. Creating credentials for connecting to the PowerSync service.
/// 2. Applying local changes against the backend application server.
///
///
open class PowerSyncBackendConnector: PowerSyncBackendConnectorProtocol {
public init() {}

open func uploadData(database: PowerSyncDatabaseProtocol) async throws {}

open func fetchCredentials() async throws -> PowerSyncCredentials? {
return nil
}
}

open func uploadData(database: PowerSyncDatabaseProtocol) async throws {}
}
4 changes: 4 additions & 0 deletions Sources/PowerSync/PowerSyncCredentials.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import Foundation


///
/// Temporary credentials to connect to the PowerSync service.
///
public struct PowerSyncCredentials: Codable {
/// PowerSync endpoint, e.g. "https://myinstance.powersync.co".
public let endpoint: String
Expand Down
10 changes: 10 additions & 0 deletions Sources/PowerSync/Schema/Column.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@ import Foundation
import PowerSyncKotlin

public protocol ColumnProtocol: Equatable {
/// Name of the column.
var name: String { get }
/// Type of the column.
///
/// If the underlying data does not match this type,
/// it is cast automatically.
///
/// For details on the cast, see:
/// https://www.sqlite.org/lang_expr.html#castexpr
///
var type: ColumnData { get }
}

Expand All @@ -12,6 +21,7 @@ public enum ColumnData {
case real
}

/// A single column in a table schema.
public struct Column: ColumnProtocol {
public let name: String
public let type: ColumnData
Expand Down
6 changes: 6 additions & 0 deletions Sources/PowerSync/Schema/Index.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import Foundation
import PowerSyncKotlin

public protocol IndexProtocol {
///
/// Descriptive name of the index.
///
var name: String { get }
///
/// List of columns used for the index.
///
var columns: [IndexedColumnProtocol] { get }
}

Expand Down
15 changes: 15 additions & 0 deletions Sources/PowerSync/Schema/IndexedColumn.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import Foundation

///
/// Describes an indexed column.
///
public protocol IndexedColumnProtocol {
///
/// Name of the column to index.
///
var column: String { get }
///
/// Whether this column is stored in ascending order in the index.
///
var ascending: Bool { get }
}

Expand All @@ -17,10 +26,16 @@ public struct IndexedColumn: IndexedColumnProtocol {
self.ascending = ascending
}

///
/// Creates ascending IndexedColumn
///
public static func ascending(_ column: String) -> IndexedColumn {
IndexedColumn(column: column, ascending: true)
}

///
/// Creates descending IndexedColumn
///
public static func descending(_ column: String) -> IndexedColumn {
IndexedColumn(column: column, ascending: false)
}
Expand Down
18 changes: 12 additions & 6 deletions Sources/PowerSync/Schema/Schema.swift
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
public protocol SchemaProtocol {
///
/// Tables used in Schema
///
var tables: [Table] { get }
///
/// Validate tables
///
func validate() throws
}

public struct Schema: SchemaProtocol {
public let tables: [Table]

public init(tables: [Table]) {
self.tables = tables
}

// Convenience initializer with variadic parameters
///
/// Convenience initializer with variadic parameters
///
public init(_ tables: Table...) {
self.init(tables: tables)
}

public func validate() throws {
var tableNames = Set<String>()

for table in tables {
if !tableNames.insert(table.name).inserted {
throw SchemaError.duplicateTableName(table.name)
Expand All @@ -30,4 +37,3 @@ public struct Schema: SchemaProtocol {
public enum SchemaError: Error {
case duplicateTableName(String)
}

58 changes: 41 additions & 17 deletions Sources/PowerSync/Schema/Table.swift
Original file line number Diff line number Diff line change
@@ -1,38 +1,59 @@
import Foundation

public protocol TableProtocol {
///
/// The synced table name, matching sync rules.
///
var name: String { get }
///
/// List of columns.
///
var columns: [Column] { get }
///
/// List of indexes.
///
var indexes: [Index] { get }
///
/// Whether the table only exists locally.
///
var localOnly: Bool { get }
///
/// Whether this is an insert-only table.
///
var insertOnly: Bool { get }
///
/// Override the name for the view
///
var viewNameOverride: String? { get }
var viewName: String { get }
}

private let MAX_AMOUNT_OF_COLUMNS = 63

///
/// A single table in the schema.
///
public struct Table: TableProtocol {
public let name: String
public let columns: [Column]
public let indexes: [Index]
public let localOnly: Bool
public let insertOnly: Bool
public let viewNameOverride: String?

public var viewName: String {
viewNameOverride ?? name
}

internal var internalName: String {
localOnly ? "ps_data_local__\(name)" : "ps_data__\(name)"
}

private let invalidSqliteCharacters = try! NSRegularExpression(
pattern: #"["'%,.#\s\[\]]"#,
options: []
)

public init(
name: String,
columns: [Column],
Expand All @@ -48,64 +69,67 @@ public struct Table: TableProtocol {
self.insertOnly = insertOnly
self.viewNameOverride = viewNameOverride
}

private func hasInvalidSqliteCharacters(_ string: String) -> Bool {
let range = NSRange(location: 0, length: string.utf16.count)
return invalidSqliteCharacters.firstMatch(in: string, options: [], range: range) != nil
}


///
/// Validate the table
///
public func validate() throws {
if columns.count > MAX_AMOUNT_OF_COLUMNS {
throw TableError.tooManyColumns(tableName: name, count: columns.count)
}

if let viewNameOverride = viewNameOverride,
hasInvalidSqliteCharacters(viewNameOverride) {
throw TableError.invalidViewName(viewName: viewNameOverride)
}

var columnNames = Set<String>(["id"])

for column in columns {
if column.name == "id" {
throw TableError.customIdColumn(tableName: name)
}

if columnNames.contains(column.name) {
throw TableError.duplicateColumn(
tableName: name,
columnName: column.name
)
}

if hasInvalidSqliteCharacters(column.name) {
throw TableError.invalidColumnName(
tableName: name,
columnName: column.name
)
}

columnNames.insert(column.name)
}

// Check indexes
var indexNames = Set<String>()

for index in indexes {
if indexNames.contains(index.name) {
throw TableError.duplicateIndex(
tableName: name,
indexName: index.name
)
}

if hasInvalidSqliteCharacters(index.name) {
throw TableError.invalidIndexName(
tableName: name,
indexName: index.name
)
}

// Check index columns exist in table
for indexColumn in index.columns {
if !columnNames.contains(indexColumn.column) {
Expand All @@ -116,7 +140,7 @@ public struct Table: TableProtocol {
)
}
}

indexNames.insert(index.name)
}
}
Expand Down