diff --git a/CHANGELOG.md b/CHANGELOG.md index df3170c..44c40a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change Log +## 13.4.0 + +* Add `total` parameter to list queries allowing skipping counting rows in a table for improved performance +* Add `Operator` class for atomic modification of rows via update, bulk update, upsert, and bulk upsert operations + ## 13.3.1 * Fix `onOpen` callback not being called when the websocket connection is established diff --git a/README.md b/README.md index 4639c71..d734a03 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Add the package to your `Package.swift` dependencies: ```swift dependencies: [ - .package(url: "git@github.com:appwrite/sdk-for-apple.git", from: "13.3.1"), + .package(url: "git@github.com:appwrite/sdk-for-apple.git", from: "13.4.0"), ], ``` diff --git a/Sources/Appwrite/Client.swift b/Sources/Appwrite/Client.swift index fc686c2..432c3df 100644 --- a/Sources/Appwrite/Client.swift +++ b/Sources/Appwrite/Client.swift @@ -23,7 +23,7 @@ open class Client { "x-sdk-name": "Apple", "x-sdk-platform": "client", "x-sdk-language": "apple", - "x-sdk-version": "13.3.1", + "x-sdk-version": "13.4.0", "x-appwrite-response-format": "1.8.0" ] diff --git a/Sources/Appwrite/Operator.swift b/Sources/Appwrite/Operator.swift new file mode 100644 index 0000000..6af290f --- /dev/null +++ b/Sources/Appwrite/Operator.swift @@ -0,0 +1,305 @@ +import Foundation + +public enum Condition: String, Codable { + case equal = "equal" + case notEqual = "notEqual" + case greaterThan = "greaterThan" + case greaterThanEqual = "greaterThanEqual" + case lessThan = "lessThan" + case lessThanEqual = "lessThanEqual" + case contains = "contains" + case isNull = "isNull" + case isNotNull = "isNotNull" +} + +enum OperatorValue: Codable { + case string(String) + case int(Int) + case double(Double) + case bool(Bool) + case array([OperatorValue]) + case null + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + if container.decodeNil() { + self = .null + } else if let stringValue = try? container.decode(String.self) { + self = .string(stringValue) + } else if let intValue = try? container.decode(Int.self) { + self = .int(intValue) + } else if let doubleValue = try? container.decode(Double.self) { + self = .double(doubleValue) + } else if let boolValue = try? container.decode(Bool.self) { + self = .bool(boolValue) + } else if let arrayValue = try? container.decode([OperatorValue].self) { + self = .array(arrayValue) + } else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "OperatorValue cannot be decoded" + ) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .string(let value): + try container.encode(value) + case .int(let value): + try container.encode(value) + case .double(let value): + try container.encode(value) + case .bool(let value): + try container.encode(value) + case .array(let value): + try container.encode(value) + case .null: + try container.encodeNil() + } + } +} + +public struct Operator : Codable, CustomStringConvertible { + var method: String + var values: [OperatorValue]? + + init(method: String, values: Any? = nil) { + self.method = method + self.values = Operator.convertToOperatorValueArray(values) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.method = try container.decode(String.self, forKey: .method) + self.values = try container.decodeIfPresent([OperatorValue].self, forKey: .values) + } + + private static func convertToOperatorValueArray(_ values: Any?) -> [OperatorValue]? { + // Handle nil + if values == nil { + return nil + } + + // Handle NSNull as [.null] + if values is NSNull { + return [.null] + } + + switch values { + case let valueArray as [OperatorValue]: + return valueArray + case let stringArray as [String]: + return stringArray.map { .string($0) } + case let intArray as [Int]: + return intArray.map { .int($0) } + case let doubleArray as [Double]: + return doubleArray.map { .double($0) } + case let boolArray as [Bool]: + return boolArray.map { .bool($0) } + case let stringValue as String: + return [.string(stringValue)] + case let intValue as Int: + return [.int(intValue)] + case let doubleValue as Double: + return [.double(doubleValue)] + case let boolValue as Bool: + return [.bool(boolValue)] + case let anyArray as [Any]: + // Preserve empty arrays as empty OperatorValue arrays + if anyArray.isEmpty { + return [] + } + + // Map all items, converting nil/unknown to .null + let nestedValues = anyArray.map { item -> OperatorValue in + if item is NSNull { + return .null + } else if let stringValue = item as? String { + return .string(stringValue) + } else if let intValue = item as? Int { + return .int(intValue) + } else if let doubleValue = item as? Double { + return .double(doubleValue) + } else if let boolValue = item as? Bool { + return .bool(boolValue) + } else if let nestedArray = item as? [Any] { + let converted = convertToOperatorValueArray(nestedArray) ?? [] + return .array(converted) + } else { + // Unknown/unsupported types become .null + return .null + } + } + return nestedValues + default: + // Unknown types become [.null] + return [.null] + } + } + + enum CodingKeys: String, CodingKey { + case method + case values + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(method, forKey: .method) + + if (values != nil) { + try container.encode(values, forKey: .values) + } + } + + public var description: String { + guard let data = try? JSONEncoder().encode(self) else { + return "" + } + + return String(data: data, encoding: .utf8) ?? "" + } + + public static func increment(_ value: Double = 1, max: Double? = nil) -> String { + if value.isNaN || value.isInfinite { + fatalError("Value cannot be NaN or Infinity") + } + if let max = max, max.isNaN || max.isInfinite { + fatalError("Max cannot be NaN or Infinity") + } + var values: [Any] = [value] + if let max = max { + values.append(max) + } + return Operator(method: "increment", values: values).description + } + + public static func decrement(_ value: Double = 1, min: Double? = nil) -> String { + if value.isNaN || value.isInfinite { + fatalError("Value cannot be NaN or Infinity") + } + if let min = min, min.isNaN || min.isInfinite { + fatalError("Min cannot be NaN or Infinity") + } + var values: [Any] = [value] + if let min = min { + values.append(min) + } + return Operator(method: "decrement", values: values).description + } + + public static func multiply(_ factor: Double, max: Double? = nil) -> String { + if factor.isNaN || factor.isInfinite { + fatalError("Factor cannot be NaN or Infinity") + } + if let max = max, max.isNaN || max.isInfinite { + fatalError("Max cannot be NaN or Infinity") + } + var values: [Any] = [factor] + if let max = max { + values.append(max) + } + return Operator(method: "multiply", values: values).description + } + + public static func divide(_ divisor: Double, min: Double? = nil) -> String { + if divisor.isNaN || divisor.isInfinite { + fatalError("Divisor cannot be NaN or Infinity") + } + if let min = min, min.isNaN || min.isInfinite { + fatalError("Min cannot be NaN or Infinity") + } + if divisor == 0 { + fatalError("Divisor cannot be zero") + } + var values: [Any] = [divisor] + if let min = min { + values.append(min) + } + return Operator(method: "divide", values: values).description + } + + public static func modulo(_ divisor: Double) -> String { + if divisor.isNaN || divisor.isInfinite { + fatalError("Divisor cannot be NaN or Infinity") + } + if divisor == 0 { + fatalError("Divisor cannot be zero") + } + return Operator(method: "modulo", values: [divisor]).description + } + + public static func power(_ exponent: Double, max: Double? = nil) -> String { + if exponent.isNaN || exponent.isInfinite { + fatalError("Exponent cannot be NaN or Infinity") + } + if let max = max, max.isNaN || max.isInfinite { + fatalError("Max cannot be NaN or Infinity") + } + var values: [Any] = [exponent] + if let max = max { + values.append(max) + } + return Operator(method: "power", values: values).description + } + + public static func arrayAppend(_ values: [Any]) -> String { + return Operator(method: "arrayAppend", values: values).description + } + + public static func arrayPrepend(_ values: [Any]) -> String { + return Operator(method: "arrayPrepend", values: values).description + } + + public static func arrayInsert(_ index: Int, value: Any) -> String { + return Operator(method: "arrayInsert", values: [index, value]).description + } + + public static func arrayRemove(_ value: Any) -> String { + return Operator(method: "arrayRemove", values: [value]).description + } + + public static func arrayUnique() -> String { + return Operator(method: "arrayUnique", values: []).description + } + + public static func arrayIntersect(_ values: [Any]) -> String { + return Operator(method: "arrayIntersect", values: values).description + } + + public static func arrayDiff(_ values: [Any]) -> String { + return Operator(method: "arrayDiff", values: values).description + } + + public static func arrayFilter(_ condition: Condition, value: Any? = nil) -> String { + let values: [Any] = [condition.rawValue, value ?? NSNull()] + return Operator(method: "arrayFilter", values: values).description + } + + public static func stringConcat(_ value: Any) -> String { + return Operator(method: "stringConcat", values: [value]).description + } + + public static func stringReplace(_ search: String, _ replace: String) -> String { + return Operator(method: "stringReplace", values: [search, replace]).description + } + + public static func toggle() -> String { + return Operator(method: "toggle", values: []).description + } + + public static func dateAddDays(_ days: Int) -> String { + return Operator(method: "dateAddDays", values: [days]).description + } + + public static func dateSubDays(_ days: Int) -> String { + return Operator(method: "dateSubDays", values: [days]).description + } + + public static func dateSetNow() -> String { + return Operator(method: "dateSetNow", values: []).description + } +} diff --git a/Sources/Appwrite/Query.swift b/Sources/Appwrite/Query.swift index e4f06b5..7610dc3 100644 --- a/Sources/Appwrite/Query.swift +++ b/Sources/Appwrite/Query.swift @@ -379,45 +379,27 @@ public struct Query : Codable, CustomStringConvertible { } public static func createdBefore(_ value: String) -> String { - return Query( - method: "createdBefore", - values: [value] - ).description + return lessThan("$createdAt", value: value) } public static func createdAfter(_ value: String) -> String { - return Query( - method: "createdAfter", - values: [value] - ).description + return greaterThan("$createdAt", value: value) } public static func createdBetween(_ start: String, _ end: String) -> String { - return Query( - method: "createdBetween", - values: [start, end] - ).description + return between("$createdAt", start: start, end: end) } public static func updatedBefore(_ value: String) -> String { - return Query( - method: "updatedBefore", - values: [value] - ).description + return lessThan("$updatedAt", value: value) } public static func updatedAfter(_ value: String) -> String { - return Query( - method: "updatedAfter", - values: [value] - ).description + return greaterThan("$updatedAt", value: value) } public static func updatedBetween(_ start: String, _ end: String) -> String { - return Query( - method: "updatedBetween", - values: [start, end] - ).description + return between("$updatedAt", start: start, end: end) } public static func or(_ queries: [String]) -> String { diff --git a/Sources/Appwrite/Services/Account.swift b/Sources/Appwrite/Services/Account.swift index 289a70f..98f46fd 100644 --- a/Sources/Appwrite/Services/Account.swift +++ b/Sources/Appwrite/Services/Account.swift @@ -208,16 +208,19 @@ open class Account: Service { /// /// - Parameters: /// - queries: [String] (optional) + /// - total: Bool (optional) /// - Throws: Exception if the request fails /// - Returns: AppwriteModels.IdentityList /// open func listIdentities( - queries: [String]? = nil + queries: [String]? = nil, + total: Bool? = nil ) async throws -> AppwriteModels.IdentityList { let apiPath: String = "/account/identities" let apiParams: [String: Any?] = [ - "queries": queries + "queries": queries, + "total": total ] let apiHeaders: [String: String] = [:] @@ -301,16 +304,19 @@ open class Account: Service { /// /// - Parameters: /// - queries: [String] (optional) + /// - total: Bool (optional) /// - Throws: Exception if the request fails /// - Returns: AppwriteModels.LogList /// open func listLogs( - queries: [String]? = nil + queries: [String]? = nil, + total: Bool? = nil ) async throws -> AppwriteModels.LogList { let apiPath: String = "/account/logs" let apiParams: [String: Any?] = [ - "queries": queries + "queries": queries, + "total": total ] let apiHeaders: [String: String] = [:] diff --git a/Sources/Appwrite/Services/Databases.swift b/Sources/Appwrite/Services/Databases.swift index f8bfc37..96d4846 100644 --- a/Sources/Appwrite/Services/Databases.swift +++ b/Sources/Appwrite/Services/Databases.swift @@ -218,6 +218,7 @@ open class Databases: Service { /// - collectionId: String /// - queries: [String] (optional) /// - transactionId: String (optional) + /// - total: Bool (optional) /// - Throws: Exception if the request fails /// - Returns: AppwriteModels.DocumentList /// @@ -227,6 +228,7 @@ open class Databases: Service { collectionId: String, queries: [String]? = nil, transactionId: String? = nil, + total: Bool? = nil, nestedType: T.Type ) async throws -> AppwriteModels.DocumentList { let apiPath: String = "/databases/{databaseId}/collections/{collectionId}/documents" @@ -235,7 +237,8 @@ open class Databases: Service { let apiParams: [String: Any?] = [ "queries": queries, - "transactionId": transactionId + "transactionId": transactionId, + "total": total ] let apiHeaders: [String: String] = [:] @@ -262,6 +265,7 @@ open class Databases: Service { /// - collectionId: String /// - queries: [String] (optional) /// - transactionId: String (optional) + /// - total: Bool (optional) /// - Throws: Exception if the request fails /// - Returns: AppwriteModels.DocumentList /// @@ -270,13 +274,15 @@ open class Databases: Service { databaseId: String, collectionId: String, queries: [String]? = nil, - transactionId: String? = nil + transactionId: String? = nil, + total: Bool? = nil ) async throws -> AppwriteModels.DocumentList<[String: AnyCodable]> { return try await listDocuments( databaseId: databaseId, collectionId: collectionId, queries: queries, transactionId: transactionId, + total: total, nestedType: [String: AnyCodable].self ) } diff --git a/Sources/Appwrite/Services/Functions.swift b/Sources/Appwrite/Services/Functions.swift index 21dea14..63431b6 100644 --- a/Sources/Appwrite/Services/Functions.swift +++ b/Sources/Appwrite/Services/Functions.swift @@ -15,18 +15,21 @@ open class Functions: Service { /// - Parameters: /// - functionId: String /// - queries: [String] (optional) + /// - total: Bool (optional) /// - Throws: Exception if the request fails /// - Returns: AppwriteModels.ExecutionList /// open func listExecutions( functionId: String, - queries: [String]? = nil + queries: [String]? = nil, + total: Bool? = nil ) async throws -> AppwriteModels.ExecutionList { let apiPath: String = "/functions/{functionId}/executions" .replacingOccurrences(of: "{functionId}", with: functionId) let apiParams: [String: Any?] = [ - "queries": queries + "queries": queries, + "total": total ] let apiHeaders: [String: String] = [:] diff --git a/Sources/Appwrite/Services/Storage.swift b/Sources/Appwrite/Services/Storage.swift index 1dd7621..46c7482 100644 --- a/Sources/Appwrite/Services/Storage.swift +++ b/Sources/Appwrite/Services/Storage.swift @@ -16,20 +16,23 @@ open class Storage: Service { /// - bucketId: String /// - queries: [String] (optional) /// - search: String (optional) + /// - total: Bool (optional) /// - Throws: Exception if the request fails /// - Returns: AppwriteModels.FileList /// open func listFiles( bucketId: String, queries: [String]? = nil, - search: String? = nil + search: String? = nil, + total: Bool? = nil ) async throws -> AppwriteModels.FileList { let apiPath: String = "/storage/buckets/{bucketId}/files" .replacingOccurrences(of: "{bucketId}", with: bucketId) let apiParams: [String: Any?] = [ "queries": queries, - "search": search + "search": search, + "total": total ] let apiHeaders: [String: String] = [:] diff --git a/Sources/Appwrite/Services/TablesDb.swift b/Sources/Appwrite/Services/TablesDb.swift index 0727dde..61cccf4 100644 --- a/Sources/Appwrite/Services/TablesDb.swift +++ b/Sources/Appwrite/Services/TablesDb.swift @@ -218,6 +218,7 @@ open class TablesDB: Service { /// - tableId: String /// - queries: [String] (optional) /// - transactionId: String (optional) + /// - total: Bool (optional) /// - Throws: Exception if the request fails /// - Returns: AppwriteModels.RowList /// @@ -226,6 +227,7 @@ open class TablesDB: Service { tableId: String, queries: [String]? = nil, transactionId: String? = nil, + total: Bool? = nil, nestedType: T.Type ) async throws -> AppwriteModels.RowList { let apiPath: String = "/tablesdb/{databaseId}/tables/{tableId}/rows" @@ -234,7 +236,8 @@ open class TablesDB: Service { let apiParams: [String: Any?] = [ "queries": queries, - "transactionId": transactionId + "transactionId": transactionId, + "total": total ] let apiHeaders: [String: String] = [:] @@ -261,6 +264,7 @@ open class TablesDB: Service { /// - tableId: String /// - queries: [String] (optional) /// - transactionId: String (optional) + /// - total: Bool (optional) /// - Throws: Exception if the request fails /// - Returns: AppwriteModels.RowList /// @@ -268,13 +272,15 @@ open class TablesDB: Service { databaseId: String, tableId: String, queries: [String]? = nil, - transactionId: String? = nil + transactionId: String? = nil, + total: Bool? = nil ) async throws -> AppwriteModels.RowList<[String: AnyCodable]> { return try await listRows( databaseId: databaseId, tableId: tableId, queries: queries, transactionId: transactionId, + total: total, nestedType: [String: AnyCodable].self ) } diff --git a/Sources/Appwrite/Services/Teams.swift b/Sources/Appwrite/Services/Teams.swift index 5441073..54744d9 100644 --- a/Sources/Appwrite/Services/Teams.swift +++ b/Sources/Appwrite/Services/Teams.swift @@ -15,19 +15,22 @@ open class Teams: Service { /// - Parameters: /// - queries: [String] (optional) /// - search: String (optional) + /// - total: Bool (optional) /// - Throws: Exception if the request fails /// - Returns: AppwriteModels.TeamList /// open func list( queries: [String]? = nil, search: String? = nil, + total: Bool? = nil, nestedType: T.Type ) async throws -> AppwriteModels.TeamList { let apiPath: String = "/teams" let apiParams: [String: Any?] = [ "queries": queries, - "search": search + "search": search, + "total": total ] let apiHeaders: [String: String] = [:] @@ -52,16 +55,19 @@ open class Teams: Service { /// - Parameters: /// - queries: [String] (optional) /// - search: String (optional) + /// - total: Bool (optional) /// - Throws: Exception if the request fails /// - Returns: AppwriteModels.TeamList /// open func list( queries: [String]? = nil, - search: String? = nil + search: String? = nil, + total: Bool? = nil ) async throws -> AppwriteModels.TeamList<[String: AnyCodable]> { return try await list( queries: queries, search: search, + total: total, nestedType: [String: AnyCodable].self ) } @@ -278,20 +284,23 @@ open class Teams: Service { /// - teamId: String /// - queries: [String] (optional) /// - search: String (optional) + /// - total: Bool (optional) /// - Throws: Exception if the request fails /// - Returns: AppwriteModels.MembershipList /// open func listMemberships( teamId: String, queries: [String]? = nil, - search: String? = nil + search: String? = nil, + total: Bool? = nil ) async throws -> AppwriteModels.MembershipList { let apiPath: String = "/teams/{teamId}/memberships" .replacingOccurrences(of: "{teamId}", with: teamId) let apiParams: [String: Any?] = [ "queries": queries, - "search": search + "search": search, + "total": total ] let apiHeaders: [String: String] = [:] diff --git a/docs/examples/account/list-identities.md b/docs/examples/account/list-identities.md index 1d3a999..eaa6cbd 100644 --- a/docs/examples/account/list-identities.md +++ b/docs/examples/account/list-identities.md @@ -7,6 +7,7 @@ let client = Client() let account = Account(client) let identityList = try await account.listIdentities( - queries: [] // optional + queries: [], // optional + total: false // optional ) diff --git a/docs/examples/account/list-logs.md b/docs/examples/account/list-logs.md index 2c42307..19a607f 100644 --- a/docs/examples/account/list-logs.md +++ b/docs/examples/account/list-logs.md @@ -7,6 +7,7 @@ let client = Client() let account = Account(client) let logList = try await account.listLogs( - queries: [] // optional + queries: [], // optional + total: false // optional ) diff --git a/docs/examples/databases/list-documents.md b/docs/examples/databases/list-documents.md index 528d999..e07c66b 100644 --- a/docs/examples/databases/list-documents.md +++ b/docs/examples/databases/list-documents.md @@ -10,6 +10,7 @@ let documentList = try await databases.listDocuments( databaseId: "", collectionId: "", queries: [], // optional - transactionId: "" // optional + transactionId: "", // optional + total: false // optional ) diff --git a/docs/examples/functions/list-executions.md b/docs/examples/functions/list-executions.md index 1636d96..50ed08d 100644 --- a/docs/examples/functions/list-executions.md +++ b/docs/examples/functions/list-executions.md @@ -8,6 +8,7 @@ let functions = Functions(client) let executionList = try await functions.listExecutions( functionId: "", - queries: [] // optional + queries: [], // optional + total: false // optional ) diff --git a/docs/examples/storage/list-files.md b/docs/examples/storage/list-files.md index 48bd0d0..66849d4 100644 --- a/docs/examples/storage/list-files.md +++ b/docs/examples/storage/list-files.md @@ -9,6 +9,7 @@ let storage = Storage(client) let fileList = try await storage.listFiles( bucketId: "", queries: [], // optional - search: "" // optional + search: "", // optional + total: false // optional ) diff --git a/docs/examples/tablesdb/list-rows.md b/docs/examples/tablesdb/list-rows.md index dee2ab9..94178aa 100644 --- a/docs/examples/tablesdb/list-rows.md +++ b/docs/examples/tablesdb/list-rows.md @@ -10,6 +10,7 @@ let rowList = try await tablesDB.listRows( databaseId: "", tableId: "", queries: [], // optional - transactionId: "" // optional + transactionId: "", // optional + total: false // optional ) diff --git a/docs/examples/teams/list-memberships.md b/docs/examples/teams/list-memberships.md index 5c8669a..c485d0b 100644 --- a/docs/examples/teams/list-memberships.md +++ b/docs/examples/teams/list-memberships.md @@ -9,6 +9,7 @@ let teams = Teams(client) let membershipList = try await teams.listMemberships( teamId: "", queries: [], // optional - search: "" // optional + search: "", // optional + total: false // optional ) diff --git a/docs/examples/teams/list.md b/docs/examples/teams/list.md index be81e9c..de209be 100644 --- a/docs/examples/teams/list.md +++ b/docs/examples/teams/list.md @@ -8,6 +8,7 @@ let teams = Teams(client) let teamList = try await teams.list( queries: [], // optional - search: "" // optional + search: "", // optional + total: false // optional )