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
## 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
* 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)
![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)
[![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)
**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)
@@ -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: "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 NIOCore
#if canImport(NIOFoundationCompat)
import NIOFoundationCompat
#endif
import NIOSSL
import Foundation
import AsyncHTTPClient
@@ -24,8 +26,8 @@ open class Client {
"x-sdk-name": "Apple",
"x-sdk-platform": "client",
"x-sdk-language": "apple",
"x-sdk-version": "15.0.0",
"x-appwrite-response-format": "1.8.0"
"x-sdk-version": "16.0.0",
"x-appwrite-response-format": "1.9.0"
]
internal var config: [String: String] = [:]
@@ -162,6 +164,51 @@ open class Client {
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
@@ -378,7 +425,7 @@ open class Client {
if data.readableBytes == 0 {
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
}
@@ -388,7 +435,7 @@ open class Client {
var responseString = ""
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
type = dict?["type"] as? String ?? ""
@@ -420,9 +467,9 @@ open class Client {
switch(input.sourceType) {
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":
input.data = ByteBuffer(data: input.data as! Data)
input.data = ByteBuffer(bytes: input.data as! Data)
default:
break
}
@@ -1,5 +1,8 @@
import Foundation
import NIO
#if canImport(NIOFoundationCompat)
import NIOFoundationCompat
#endif
import NIOHTTP1
import NIOWebSocket
@@ -18,7 +21,7 @@ class MessageHandler {
self.client = client
self.buffer = ByteBufferAllocator().buffer(capacity: 0)
}
private func unmaskedData(frame: WebSocketFrame) -> ByteBuffer {
var frameData = frame.data
if let maskingKey = frame.maskKey {
@@ -31,7 +34,7 @@ class MessageHandler {
extension MessageHandler: ChannelInboundHandler, RemovableChannelHandler {
typealias InboundIn = WebSocketFrame
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
let frame = self.unwrapInboundIn(data)
switch frame.opcode {
@@ -56,18 +59,14 @@ extension MessageHandler: ChannelInboundHandler, RemovableChannelHandler {
case .binary:
let data = unmaskedData(frame: frame)
if frame.fin {
guard let binaryData = data.getData(at: 0, length: data.readableBytes) else {
return
}
let binaryData = Data(data.readableBytesView)
if let delegate = client.delegate {
try! delegate.onMessage(data: binaryData)
} else {
client.onBinaryMessage(binaryData)
}
} else {
guard let binaryData = data.getData(at: 0, length: data.readableBytes) else {
return
}
let binaryData = Data(data.readableBytesView)
binaryBuffer = binaryData
}
case .continuation:
@@ -92,9 +91,7 @@ extension MessageHandler: ChannelInboundHandler, RemovableChannelHandler {
}
} else {
if frame.fin {
guard let binaryData = data.getData(at: 0, length: data.readableBytes) else {
return
}
let binaryData = Data(data.readableBytesView)
binaryBuffer.append(binaryData)
if let delegate = client.delegate {
try! delegate.onMessage(data: binaryBuffer)
@@ -102,9 +99,7 @@ extension MessageHandler: ChannelInboundHandler, RemovableChannelHandler {
client.onBinaryMessage(binaryBuffer)
}
} else {
guard let binaryData = data.getData(at: 0, length: data.readableBytes) else {
return
}
let binaryData = Data(data.readableBytesView)
binaryBuffer.append(binaryData)
}
}
@@ -114,18 +109,18 @@ extension MessageHandler: ChannelInboundHandler, RemovableChannelHandler {
}
let data = frame.data
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 {
delegate.onClose(channel: context.channel, data: data.getData(at: 0, length: data.readableBytes)!)
delegate.onClose(channel: context.channel, data: Data(data.readableBytesView))
} else {
client.onClose(context.channel, data.getData(at: 0, length: data.readableBytes)!)
client.onClose(context.channel, Data(data.readableBytesView))
}
default:
break
}
}
public func errorCaught(context: ChannelHandlerContext, error: Swift.Error) {
if client.delegate != nil {
try! client.delegate?.onError(error: error, status: nil)
@@ -1,9 +1,11 @@
import Foundation
import NIO
#if canImport(NIOFoundationCompat)
import NIOFoundationCompat
#endif
import NIOHTTP1
import NIOWebSocket
import Dispatch
import NIOFoundationCompat
import NIOSSL
public let WEBSOCKET_LOCKER_QUEUE = "SyncLocker"
+4 -4
View File
@@ -18,7 +18,7 @@ open class Document<T : Codable>: Codable {
/// Document ID.
public let id: String
/// Document sequence ID.
public let sequence: Int
public let sequence: String
/// Collection ID.
public let collectionId: String
/// Database ID.
@@ -34,7 +34,7 @@ open class Document<T : Codable>: Codable {
init(
id: String,
sequence: Int,
sequence: String,
collectionId: String,
databaseId: String,
createdAt: String,
@@ -56,7 +56,7 @@ open class Document<T : Codable>: Codable {
let container = try decoder.container(keyedBy: CodingKeys.self)
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.databaseId = try container.decode(String.self, forKey: .databaseId)
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 {
return Document(
id: map["$id"] as? String ?? "",
sequence: map["$sequence"] as? Int ?? 0,
sequence: map["$sequence"] as? String ?? "",
collectionId: map["$collectionId"] as? String ?? "",
databaseId: map["$databaseId"] as? String ?? "",
createdAt: map["$createdAt"] as? String ?? "",
+3 -3
View File
@@ -30,11 +30,11 @@ open class Log: Codable {
/// Event name.
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
/// User Email.
/// User email of the actor recorded for this log. During impersonation, this is the original impersonator.
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
/// API mode when event triggered.
public let mode: String
+4 -4
View File
@@ -18,7 +18,7 @@ open class Row<T : Codable>: Codable {
/// Row ID.
public let id: String
/// Row sequence ID.
public let sequence: Int
public let sequence: String
/// Table ID.
public let tableId: String
/// Database ID.
@@ -34,7 +34,7 @@ open class Row<T : Codable>: Codable {
init(
id: String,
sequence: Int,
sequence: String,
tableId: String,
databaseId: String,
createdAt: String,
@@ -56,7 +56,7 @@ open class Row<T : Codable>: Codable {
let container = try decoder.container(keyedBy: CodingKeys.self)
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.databaseId = try container.decode(String.self, forKey: .databaseId)
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 {
return Row(
id: map["$id"] as! String,
sequence: map["$sequence"] as! Int,
sequence: map["$sequence"] as! String,
tableId: map["$tableId"] as! String,
databaseId: map["$databaseId"] 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 targets = "targets"
case accessedAt = "accessedAt"
case impersonator = "impersonator"
case impersonatorUserId = "impersonatorUserId"
}
/// User ID.
@@ -64,6 +66,10 @@ open class User<T : Codable>: Codable {
public let targets: [Target]
/// Most recent access date in ISO 8601 format. This attribute is only updated again after 24 hours.
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(
id: String,
@@ -84,7 +90,9 @@ open class User<T : Codable>: Codable {
mfa: Bool,
prefs: Preferences<T>,
targets: [Target],
accessedAt: String
accessedAt: String,
impersonator: Bool?,
impersonatorUserId: String?
) {
self.id = id
self.createdAt = createdAt
@@ -105,6 +113,8 @@ open class User<T : Codable>: Codable {
self.prefs = prefs
self.targets = targets
self.accessedAt = accessedAt
self.impersonator = impersonator
self.impersonatorUserId = impersonatorUserId
}
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.targets = try container.decode([Target].self, forKey: .targets)
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 {
@@ -153,6 +165,8 @@ open class User<T : Codable>: Codable {
try container.encode(prefs, forKey: .prefs)
try container.encode(targets, forKey: .targets)
try container.encode(accessedAt, forKey: .accessedAt)
try container.encodeIfPresent(impersonator, forKey: .impersonator)
try container.encodeIfPresent(impersonatorUserId, forKey: .impersonatorUserId)
}
public func toMap() -> [String: Any] {
@@ -175,7 +189,9 @@ open class User<T : Codable>: Codable {
"mfa": mfa as Any,
"prefs": prefs.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,
prefs: Preferences.from(map: map["prefs"] as! [String: Any]),
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
)
DispatchQueue.main.async {
self.downloadedImage = Image(data: Data(buffer: data))
self.downloadedImage = Image(data: Data(data.readableBytesView))
}
} catch {
DispatchQueue.main.async {
@@ -87,7 +87,7 @@ class ViewController: UIViewController {
bucketId: bucketId,
fileId: fileId
)
let data = response.getData(at: 0, length: response.readableBytes)!
let data = Data(response.readableBytesView)
self.image.image = UIImage(data: data)
} catch {
self.response = String(describing: error)