Merge pull request #108 from appwrite/dev

This commit is contained in:
Jake Barnby
2026-03-26 06:22:05 +00:00
committed by GitHub
11 changed files with 118 additions and 44 deletions
+12
View File
@@ -1,5 +1,17 @@
# Change Log # Change Log
## 16.0.0
* [BREAKING] Changed `$sequence` type from `Int` to `String` for `Row` and `Document` models
* Added impersonation support: `setImpersonateUserId()`, `setImpersonateUserEmail()`, `setImpersonateUserPhone()` on `Client`
* Added `impersonator` and `impersonatorUserId` optional fields to `User` model
* Updated `Log` model field descriptions to clarify impersonation behavior for `userId`, `userEmail`, `userName`
* Fixed `NIOFoundationCompat` import to be conditional with `#if canImport` for platform compatibility
* Fixed `ByteBuffer` to `Data` conversion to use `readableBytesView` throughout (Client and WebSocket handler)
* Fixed `ByteBuffer(data:)` calls replaced with `ByteBuffer(bytes:)` for file/data loading
* Updated `X-Appwrite-Response-Format` header to `1.9.0`
* Updated API version badge to `1.9.0` and compatibility note to server version `1.9.x` in README
## 15.0.0 ## 15.0.0
* Breaking: RealtimeChannel API required explicit IDs and threw errors. * Breaking: RealtimeChannel API required explicit IDs and threw errors.
+3 -3
View File
@@ -2,12 +2,12 @@
![Swift Package Manager](https://img.shields.io/github/v/release/appwrite/sdk-for-apple.svg?color=green&style=flat-square) ![Swift Package Manager](https://img.shields.io/github/v/release/appwrite/sdk-for-apple.svg?color=green&style=flat-square)
![License](https://img.shields.io/github/license/appwrite/sdk-for-apple.svg?style=flat-square) ![License](https://img.shields.io/github/license/appwrite/sdk-for-apple.svg?style=flat-square)
![Version](https://img.shields.io/badge/api%20version-1.8.1-blue.svg?style=flat-square) ![Version](https://img.shields.io/badge/api%20version-1.9.0-blue.svg?style=flat-square)
[![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) [![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator)
[![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) [![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite)
[![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord)
**This SDK is compatible with Appwrite server version latest. For older versions, please check [previous releases](https://github.com/appwrite/sdk-for-apple/releases).** **This SDK is compatible with Appwrite server version 1.9.x. For older versions, please check [previous releases](https://github.com/appwrite/sdk-for-apple/releases).**
Appwrite is an open-source backend as a service server that abstracts and simplifies complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Apple SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs) Appwrite is an open-source backend as a service server that abstracts and simplifies complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the Apple SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs)
@@ -31,7 +31,7 @@ Add the package to your `Package.swift` dependencies:
```swift ```swift
dependencies: [ dependencies: [
.package(url: "git@github.com:appwrite/sdk-for-apple.git", from: "15.0.0"), .package(url: "git@github.com:appwrite/sdk-for-apple.git", from: "16.0.0"),
], ],
``` ```
+53 -6
View File
@@ -1,6 +1,8 @@
import NIO import NIO
import NIOCore import NIOCore
#if canImport(NIOFoundationCompat)
import NIOFoundationCompat import NIOFoundationCompat
#endif
import NIOSSL import NIOSSL
import Foundation import Foundation
import AsyncHTTPClient import AsyncHTTPClient
@@ -24,8 +26,8 @@ open class Client {
"x-sdk-name": "Apple", "x-sdk-name": "Apple",
"x-sdk-platform": "client", "x-sdk-platform": "client",
"x-sdk-language": "apple", "x-sdk-language": "apple",
"x-sdk-version": "15.0.0", "x-sdk-version": "16.0.0",
"x-appwrite-response-format": "1.8.0" "x-appwrite-response-format": "1.9.0"
] ]
internal var config: [String: String] = [:] internal var config: [String: String] = [:]
@@ -162,6 +164,51 @@ open class Client {
return self return self
} }
///
/// Set ImpersonateUserId
///
/// Impersonate a user by ID on an already user-authenticated request. Requires the current request to be authenticated as a user with impersonator capability; X-Appwrite-Key alone is not sufficient. Impersonator users are intentionally granted users.read so they can discover a target before impersonation begins. Internal audit logs still attribute actions to the original impersonator and record the impersonated target only in internal audit payload data.
///
/// @param String value
///
/// @return Client
///
open func setImpersonateUserId(_ value: String) -> Client {
config["impersonateuserid"] = value
_ = addHeader(key: "X-Appwrite-Impersonate-User-Id", value: value)
return self
}
///
/// Set ImpersonateUserEmail
///
/// Impersonate a user by email on an already user-authenticated request. Requires the current request to be authenticated as a user with impersonator capability; X-Appwrite-Key alone is not sufficient. Impersonator users are intentionally granted users.read so they can discover a target before impersonation begins. Internal audit logs still attribute actions to the original impersonator and record the impersonated target only in internal audit payload data.
///
/// @param String value
///
/// @return Client
///
open func setImpersonateUserEmail(_ value: String) -> Client {
config["impersonateuseremail"] = value
_ = addHeader(key: "X-Appwrite-Impersonate-User-Email", value: value)
return self
}
///
/// Set ImpersonateUserPhone
///
/// Impersonate a user by phone on an already user-authenticated request. Requires the current request to be authenticated as a user with impersonator capability; X-Appwrite-Key alone is not sufficient. Impersonator users are intentionally granted users.read so they can discover a target before impersonation begins. Internal audit logs still attribute actions to the original impersonator and record the impersonated target only in internal audit payload data.
///
/// @param String value
///
/// @return Client
///
open func setImpersonateUserPhone(_ value: String) -> Client {
config["impersonateuserphone"] = value
_ = addHeader(key: "X-Appwrite-Impersonate-User-Phone", value: value)
return self
}
/// ///
/// Set self signed /// Set self signed
@@ -378,7 +425,7 @@ open class Client {
if data.readableBytes == 0 { if data.readableBytes == 0 {
return true as! T return true as! T
} }
let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] let dict = try JSONSerialization.jsonObject(with: Data(data.readableBytesView)) as? [String: Any]
return converter?(dict!) ?? dict! as! T return converter?(dict!) ?? dict! as! T
} }
@@ -388,7 +435,7 @@ open class Client {
var responseString = "" var responseString = ""
do { do {
let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] let dict = try JSONSerialization.jsonObject(with: Data(data.readableBytesView)) as? [String: Any]
message = dict?["message"] as? String ?? response.status.reasonPhrase message = dict?["message"] as? String ?? response.status.reasonPhrase
type = dict?["type"] as? String ?? "" type = dict?["type"] as? String ?? ""
@@ -420,9 +467,9 @@ open class Client {
switch(input.sourceType) { switch(input.sourceType) {
case "path": case "path":
input.data = ByteBuffer(data: try! Data(contentsOf: URL(fileURLWithPath: input.path))) input.data = ByteBuffer(bytes: try! Data(contentsOf: URL(fileURLWithPath: input.path)))
case "data": case "data":
input.data = ByteBuffer(data: input.data as! Data) input.data = ByteBuffer(bytes: input.data as! Data)
default: default:
break break
} }
@@ -1,5 +1,8 @@
import Foundation import Foundation
import NIO import NIO
#if canImport(NIOFoundationCompat)
import NIOFoundationCompat
#endif
import NIOHTTP1 import NIOHTTP1
import NIOWebSocket import NIOWebSocket
@@ -18,7 +21,7 @@ class MessageHandler {
self.client = client self.client = client
self.buffer = ByteBufferAllocator().buffer(capacity: 0) self.buffer = ByteBufferAllocator().buffer(capacity: 0)
} }
private func unmaskedData(frame: WebSocketFrame) -> ByteBuffer { private func unmaskedData(frame: WebSocketFrame) -> ByteBuffer {
var frameData = frame.data var frameData = frame.data
if let maskingKey = frame.maskKey { if let maskingKey = frame.maskKey {
@@ -31,7 +34,7 @@ class MessageHandler {
extension MessageHandler: ChannelInboundHandler, RemovableChannelHandler { extension MessageHandler: ChannelInboundHandler, RemovableChannelHandler {
typealias InboundIn = WebSocketFrame typealias InboundIn = WebSocketFrame
public func channelRead(context: ChannelHandlerContext, data: NIOAny) { public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let frame = self.unwrapInboundIn(data) let frame = self.unwrapInboundIn(data)
switch frame.opcode { switch frame.opcode {
@@ -56,18 +59,14 @@ extension MessageHandler: ChannelInboundHandler, RemovableChannelHandler {
case .binary: case .binary:
let data = unmaskedData(frame: frame) let data = unmaskedData(frame: frame)
if frame.fin { if frame.fin {
guard let binaryData = data.getData(at: 0, length: data.readableBytes) else { let binaryData = Data(data.readableBytesView)
return
}
if let delegate = client.delegate { if let delegate = client.delegate {
try! delegate.onMessage(data: binaryData) try! delegate.onMessage(data: binaryData)
} else { } else {
client.onBinaryMessage(binaryData) client.onBinaryMessage(binaryData)
} }
} else { } else {
guard let binaryData = data.getData(at: 0, length: data.readableBytes) else { let binaryData = Data(data.readableBytesView)
return
}
binaryBuffer = binaryData binaryBuffer = binaryData
} }
case .continuation: case .continuation:
@@ -92,9 +91,7 @@ extension MessageHandler: ChannelInboundHandler, RemovableChannelHandler {
} }
} else { } else {
if frame.fin { if frame.fin {
guard let binaryData = data.getData(at: 0, length: data.readableBytes) else { let binaryData = Data(data.readableBytesView)
return
}
binaryBuffer.append(binaryData) binaryBuffer.append(binaryData)
if let delegate = client.delegate { if let delegate = client.delegate {
try! delegate.onMessage(data: binaryBuffer) try! delegate.onMessage(data: binaryBuffer)
@@ -102,9 +99,7 @@ extension MessageHandler: ChannelInboundHandler, RemovableChannelHandler {
client.onBinaryMessage(binaryBuffer) client.onBinaryMessage(binaryBuffer)
} }
} else { } else {
guard let binaryData = data.getData(at: 0, length: data.readableBytes) else { let binaryData = Data(data.readableBytesView)
return
}
binaryBuffer.append(binaryData) binaryBuffer.append(binaryData)
} }
} }
@@ -114,18 +109,18 @@ extension MessageHandler: ChannelInboundHandler, RemovableChannelHandler {
} }
let data = frame.data let data = frame.data
if !client.closeSent { if !client.closeSent {
client.close(data: frame.data.getData(at: 0, length: frame.data.readableBytes) ?? Data()) client.close(data: Data(frame.data.readableBytesView))
} }
if let delegate = client.delegate { if let delegate = client.delegate {
delegate.onClose(channel: context.channel, data: data.getData(at: 0, length: data.readableBytes)!) delegate.onClose(channel: context.channel, data: Data(data.readableBytesView))
} else { } else {
client.onClose(context.channel, data.getData(at: 0, length: data.readableBytes)!) client.onClose(context.channel, Data(data.readableBytesView))
} }
default: default:
break break
} }
} }
public func errorCaught(context: ChannelHandlerContext, error: Swift.Error) { public func errorCaught(context: ChannelHandlerContext, error: Swift.Error) {
if client.delegate != nil { if client.delegate != nil {
try! client.delegate?.onError(error: error, status: nil) try! client.delegate?.onError(error: error, status: nil)
@@ -1,9 +1,11 @@
import Foundation import Foundation
import NIO import NIO
#if canImport(NIOFoundationCompat)
import NIOFoundationCompat
#endif
import NIOHTTP1 import NIOHTTP1
import NIOWebSocket import NIOWebSocket
import Dispatch import Dispatch
import NIOFoundationCompat
import NIOSSL import NIOSSL
public let WEBSOCKET_LOCKER_QUEUE = "SyncLocker" public let WEBSOCKET_LOCKER_QUEUE = "SyncLocker"
+4 -4
View File
@@ -18,7 +18,7 @@ open class Document<T : Codable>: Codable {
/// Document ID. /// Document ID.
public let id: String public let id: String
/// Document sequence ID. /// Document sequence ID.
public let sequence: Int public let sequence: String
/// Collection ID. /// Collection ID.
public let collectionId: String public let collectionId: String
/// Database ID. /// Database ID.
@@ -34,7 +34,7 @@ open class Document<T : Codable>: Codable {
init( init(
id: String, id: String,
sequence: Int, sequence: String,
collectionId: String, collectionId: String,
databaseId: String, databaseId: String,
createdAt: String, createdAt: String,
@@ -56,7 +56,7 @@ open class Document<T : Codable>: Codable {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id) self.id = try container.decode(String.self, forKey: .id)
self.sequence = try container.decode(Int.self, forKey: .sequence) self.sequence = try container.decode(String.self, forKey: .sequence)
self.collectionId = try container.decode(String.self, forKey: .collectionId) self.collectionId = try container.decode(String.self, forKey: .collectionId)
self.databaseId = try container.decode(String.self, forKey: .databaseId) self.databaseId = try container.decode(String.self, forKey: .databaseId)
self.createdAt = try container.decode(String.self, forKey: .createdAt) self.createdAt = try container.decode(String.self, forKey: .createdAt)
@@ -94,7 +94,7 @@ open class Document<T : Codable>: Codable {
public static func from(map: [String: Any] ) -> Document { public static func from(map: [String: Any] ) -> Document {
return Document( return Document(
id: map["$id"] as? String ?? "", id: map["$id"] as? String ?? "",
sequence: map["$sequence"] as? Int ?? 0, sequence: map["$sequence"] as? String ?? "",
collectionId: map["$collectionId"] as? String ?? "", collectionId: map["$collectionId"] as? String ?? "",
databaseId: map["$databaseId"] as? String ?? "", databaseId: map["$databaseId"] as? String ?? "",
createdAt: map["$createdAt"] as? String ?? "", createdAt: map["$createdAt"] as? String ?? "",
+3 -3
View File
@@ -30,11 +30,11 @@ open class Log: Codable {
/// Event name. /// Event name.
public let event: String public let event: String
/// User ID. /// User ID of the actor recorded for this log. During impersonation, this is the original impersonator, not the impersonated target user.
public let userId: String public let userId: String
/// User Email. /// User email of the actor recorded for this log. During impersonation, this is the original impersonator.
public let userEmail: String public let userEmail: String
/// User Name. /// User name of the actor recorded for this log. During impersonation, this is the original impersonator.
public let userName: String public let userName: String
/// API mode when event triggered. /// API mode when event triggered.
public let mode: String public let mode: String
+4 -4
View File
@@ -18,7 +18,7 @@ open class Row<T : Codable>: Codable {
/// Row ID. /// Row ID.
public let id: String public let id: String
/// Row sequence ID. /// Row sequence ID.
public let sequence: Int public let sequence: String
/// Table ID. /// Table ID.
public let tableId: String public let tableId: String
/// Database ID. /// Database ID.
@@ -34,7 +34,7 @@ open class Row<T : Codable>: Codable {
init( init(
id: String, id: String,
sequence: Int, sequence: String,
tableId: String, tableId: String,
databaseId: String, databaseId: String,
createdAt: String, createdAt: String,
@@ -56,7 +56,7 @@ open class Row<T : Codable>: Codable {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id) self.id = try container.decode(String.self, forKey: .id)
self.sequence = try container.decode(Int.self, forKey: .sequence) self.sequence = try container.decode(String.self, forKey: .sequence)
self.tableId = try container.decode(String.self, forKey: .tableId) self.tableId = try container.decode(String.self, forKey: .tableId)
self.databaseId = try container.decode(String.self, forKey: .databaseId) self.databaseId = try container.decode(String.self, forKey: .databaseId)
self.createdAt = try container.decode(String.self, forKey: .createdAt) self.createdAt = try container.decode(String.self, forKey: .createdAt)
@@ -94,7 +94,7 @@ open class Row<T : Codable>: Codable {
public static func from(map: [String: Any] ) -> Row { public static func from(map: [String: Any] ) -> Row {
return Row( return Row(
id: map["$id"] as! String, id: map["$id"] as! String,
sequence: map["$sequence"] as! Int, sequence: map["$sequence"] as! String,
tableId: map["$tableId"] as! String, tableId: map["$tableId"] as! String,
databaseId: map["$databaseId"] as! String, databaseId: map["$databaseId"] as! String,
createdAt: map["$createdAt"] as! String, createdAt: map["$createdAt"] as! String,
+21 -3
View File
@@ -24,6 +24,8 @@ open class User<T : Codable>: Codable {
case prefs = "prefs" case prefs = "prefs"
case targets = "targets" case targets = "targets"
case accessedAt = "accessedAt" case accessedAt = "accessedAt"
case impersonator = "impersonator"
case impersonatorUserId = "impersonatorUserId"
} }
/// User ID. /// User ID.
@@ -64,6 +66,10 @@ open class User<T : Codable>: Codable {
public let targets: [Target] public let targets: [Target]
/// Most recent access date in ISO 8601 format. This attribute is only updated again after 24 hours. /// Most recent access date in ISO 8601 format. This attribute is only updated again after 24 hours.
public let accessedAt: String public let accessedAt: String
/// Whether the user can impersonate other users.
public let impersonator: Bool?
/// ID of the original actor performing the impersonation. Present only when the current request is impersonating another user. Internal audit logs attribute the action to this user, while the impersonated target is recorded only in internal audit payload data.
public let impersonatorUserId: String?
init( init(
id: String, id: String,
@@ -84,7 +90,9 @@ open class User<T : Codable>: Codable {
mfa: Bool, mfa: Bool,
prefs: Preferences<T>, prefs: Preferences<T>,
targets: [Target], targets: [Target],
accessedAt: String accessedAt: String,
impersonator: Bool?,
impersonatorUserId: String?
) { ) {
self.id = id self.id = id
self.createdAt = createdAt self.createdAt = createdAt
@@ -105,6 +113,8 @@ open class User<T : Codable>: Codable {
self.prefs = prefs self.prefs = prefs
self.targets = targets self.targets = targets
self.accessedAt = accessedAt self.accessedAt = accessedAt
self.impersonator = impersonator
self.impersonatorUserId = impersonatorUserId
} }
public required init(from decoder: Decoder) throws { public required init(from decoder: Decoder) throws {
@@ -129,6 +139,8 @@ open class User<T : Codable>: Codable {
self.prefs = try container.decode(Preferences<T>.self, forKey: .prefs) self.prefs = try container.decode(Preferences<T>.self, forKey: .prefs)
self.targets = try container.decode([Target].self, forKey: .targets) self.targets = try container.decode([Target].self, forKey: .targets)
self.accessedAt = try container.decode(String.self, forKey: .accessedAt) self.accessedAt = try container.decode(String.self, forKey: .accessedAt)
self.impersonator = try container.decodeIfPresent(Bool.self, forKey: .impersonator)
self.impersonatorUserId = try container.decodeIfPresent(String.self, forKey: .impersonatorUserId)
} }
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
@@ -153,6 +165,8 @@ open class User<T : Codable>: Codable {
try container.encode(prefs, forKey: .prefs) try container.encode(prefs, forKey: .prefs)
try container.encode(targets, forKey: .targets) try container.encode(targets, forKey: .targets)
try container.encode(accessedAt, forKey: .accessedAt) try container.encode(accessedAt, forKey: .accessedAt)
try container.encodeIfPresent(impersonator, forKey: .impersonator)
try container.encodeIfPresent(impersonatorUserId, forKey: .impersonatorUserId)
} }
public func toMap() -> [String: Any] { public func toMap() -> [String: Any] {
@@ -175,7 +189,9 @@ open class User<T : Codable>: Codable {
"mfa": mfa as Any, "mfa": mfa as Any,
"prefs": prefs.toMap() as Any, "prefs": prefs.toMap() as Any,
"targets": targets.map { $0.toMap() } as Any, "targets": targets.map { $0.toMap() } as Any,
"accessedAt": accessedAt as Any "accessedAt": accessedAt as Any,
"impersonator": impersonator as Any,
"impersonatorUserId": impersonatorUserId as Any
] ]
} }
@@ -199,7 +215,9 @@ open class User<T : Codable>: Codable {
mfa: map["mfa"] as! Bool, mfa: map["mfa"] as! Bool,
prefs: Preferences.from(map: map["prefs"] as! [String: Any]), prefs: Preferences.from(map: map["prefs"] as! [String: Any]),
targets: (map["targets"] as! [[String: Any]]).map { Target.from(map: $0) }, targets: (map["targets"] as! [[String: Any]]).map { Target.from(map: $0) },
accessedAt: map["accessedAt"] as! String accessedAt: map["accessedAt"] as! String,
impersonator: map["impersonator"] as? Bool,
impersonatorUserId: map["impersonatorUserId"] as? String
) )
} }
} }
@@ -90,7 +90,7 @@ extension ExampleView {
fileId: fileId fileId: fileId
) )
DispatchQueue.main.async { DispatchQueue.main.async {
self.downloadedImage = Image(data: Data(buffer: data)) self.downloadedImage = Image(data: Data(data.readableBytesView))
} }
} catch { } catch {
DispatchQueue.main.async { DispatchQueue.main.async {
@@ -87,7 +87,7 @@ class ViewController: UIViewController {
bucketId: bucketId, bucketId: bucketId,
fileId: fileId fileId: fileId
) )
let data = response.getData(at: 0, length: response.readableBytes)! let data = Data(response.readableBytesView)
self.image.image = UIImage(data: data) self.image.image = UIImage(data: data)
} catch { } catch {
self.response = String(describing: error) self.response = String(describing: error)