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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Change Log

## 18.0.0

* Breaking: `avatars.getScreenshot` `theme` parameter now uses the `BrowserTheme` enum
* Breaking: Removed generic type parameters from `presences` service methods
* Added: `BrowserTheme` enum
* Updated: `Presence` model is now concrete and adds a `metadata` field

## 17.1.1

* Fixed: Removed `Advisor` service and `Insight`, `InsightCTA`, `InsightList`, `Report`, `ReportList` models (admin-only endpoints, not intended for client SDKs)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: "17.2.0"),
.package(url: "git@github.com:appwrite/sdk-for-apple.git", from: "18.0.0"),
],
```

Expand Down
2 changes: 1 addition & 1 deletion Sources/Appwrite/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ open class Client {
"x-sdk-name": "Apple",
"x-sdk-platform": "client",
"x-sdk-language": "apple",
"x-sdk-version": "17.2.0",
"x-sdk-version": "18.0.0",
"x-appwrite-response-format": "1.9.5"
]

Expand Down
4 changes: 2 additions & 2 deletions Sources/Appwrite/Services/Avatars.swift
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ open class Avatars: Service {
/// - viewportWidth: Int (optional)
/// - viewportHeight: Int (optional)
/// - scale: Double (optional)
/// - theme: AppwriteEnums.Theme (optional)
/// - theme: AppwriteEnums.BrowserTheme (optional)
/// - userAgent: String (optional)
/// - fullpage: Bool (optional)
/// - locale: String (optional)
Expand All @@ -332,7 +332,7 @@ open class Avatars: Service {
viewportWidth: Int? = nil,
viewportHeight: Int? = nil,
scale: Double? = nil,
theme: AppwriteEnums.Theme? = nil,
theme: AppwriteEnums.BrowserTheme? = nil,
userAgent: String? = nil,
fullpage: Bool? = nil,
locale: String? = nil,
Expand Down
151 changes: 20 additions & 131 deletions Sources/Appwrite/Services/Presences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@ open class Presences: Service {
/// - total: Bool (optional)
/// - ttl: Int (optional)
/// - Throws: Exception if the request fails
/// - Returns: AppwriteModels.PresenceList<T>
/// - Returns: AppwriteModels.PresenceList
///
open func list<T>(
open func list(
queries: [String]? = nil,
total: Bool? = nil,
ttl: Int? = nil,
nestedType: T.Type
) async throws -> AppwriteModels.PresenceList<T> {
ttl: Int? = nil
) async throws -> AppwriteModels.PresenceList {
let apiPath: String = "/presences"

let apiParams: [String: Any?] = [
Expand All @@ -35,7 +34,7 @@ open class Presences: Service {

let apiHeaders: [String: String] = [:]

let converter: (Any) throws -> AppwriteModels.PresenceList<T> = { response in
let converter: (Any) throws -> AppwriteModels.PresenceList = { response in
return AppwriteModels.PresenceList.from(map: response as! [String: Any])
}

Expand All @@ -48,30 +47,6 @@ open class Presences: Service {
)
}

///
/// List presence logs. Expired entries are filtered out automatically.
///
///
/// - Parameters:
/// - queries: [String] (optional)
/// - total: Bool (optional)
/// - ttl: Int (optional)
/// - Throws: Exception if the request fails
/// - Returns: AppwriteModels.PresenceList<T>
///
open func list(
queries: [String]? = nil,
total: Bool? = nil,
ttl: Int? = nil
) async throws -> AppwriteModels.PresenceList<[String: AnyCodable]> {
return try await list(
queries: queries,
total: total,
ttl: ttl,
nestedType: [String: AnyCodable].self
)
}

///
/// Get a presence log by its unique ID. Entries whose `expiresAt` is in the
/// past are treated as not found.
Expand All @@ -80,20 +55,19 @@ open class Presences: Service {
/// - Parameters:
/// - presenceId: String
/// - Throws: Exception if the request fails
/// - Returns: AppwriteModels.Presence<T>
/// - Returns: AppwriteModels.Presence
///
open func get<T>(
presenceId: String,
nestedType: T.Type
) async throws -> AppwriteModels.Presence<T> {
open func get(
presenceId: String
) async throws -> AppwriteModels.Presence {
let apiPath: String = "/presences/{presenceId}"
.replacingOccurrences(of: "{presenceId}", with: presenceId)

let apiParams: [String: Any] = [:]

let apiHeaders: [String: String] = [:]

let converter: (Any) throws -> AppwriteModels.Presence<T> = { response in
let converter: (Any) throws -> AppwriteModels.Presence = { response in
return AppwriteModels.Presence.from(map: response as! [String: Any])
}

Expand All @@ -106,25 +80,6 @@ open class Presences: Service {
)
}

///
/// Get a presence log by its unique ID. Entries whose `expiresAt` is in the
/// past are treated as not found.
///
///
/// - Parameters:
/// - presenceId: String
/// - Throws: Exception if the request fails
/// - Returns: AppwriteModels.Presence<T>
///
open func get(
presenceId: String
) async throws -> AppwriteModels.Presence<[String: AnyCodable]> {
return try await get(
presenceId: presenceId,
nestedType: [String: AnyCodable].self
)
}

///
/// Create or update a presence log by its user ID.
///
Expand All @@ -136,16 +91,15 @@ open class Presences: Service {
/// - expiresAt: String (optional)
/// - metadata: Any (optional)
/// - Throws: Exception if the request fails
/// - Returns: AppwriteModels.Presence<T>
/// - Returns: AppwriteModels.Presence
///
open func upsert<T>(
open func upsert(
presenceId: String,
status: String,
permissions: [String]? = nil,
expiresAt: String? = nil,
metadata: Any? = nil,
nestedType: T.Type
) async throws -> AppwriteModels.Presence<T> {
metadata: Any? = nil
) async throws -> AppwriteModels.Presence {
let apiPath: String = "/presences/{presenceId}"
.replacingOccurrences(of: "{presenceId}", with: presenceId)

Expand All @@ -160,7 +114,7 @@ open class Presences: Service {
"content-type": "application/json"
]

let converter: (Any) throws -> AppwriteModels.Presence<T> = { response in
let converter: (Any) throws -> AppwriteModels.Presence = { response in
return AppwriteModels.Presence.from(map: response as! [String: Any])
}

Expand All @@ -173,36 +127,6 @@ open class Presences: Service {
)
}

///
/// Create or update a presence log by its user ID.
///
///
/// - Parameters:
/// - presenceId: String
/// - status: String
/// - permissions: [String] (optional)
/// - expiresAt: String (optional)
/// - metadata: Any (optional)
/// - Throws: Exception if the request fails
/// - Returns: AppwriteModels.Presence<T>
///
open func upsert(
presenceId: String,
status: String,
permissions: [String]? = nil,
expiresAt: String? = nil,
metadata: Any? = nil
) async throws -> AppwriteModels.Presence<[String: AnyCodable]> {
return try await upsert(
presenceId: presenceId,
status: status,
permissions: permissions,
expiresAt: expiresAt,
metadata: metadata,
nestedType: [String: AnyCodable].self
)
}

///
/// Update a presence log by its unique ID. Using the patch method you can pass
/// only specific fields that will get updated.
Expand All @@ -216,17 +140,16 @@ open class Presences: Service {
/// - permissions: [String] (optional)
/// - purge: Bool (optional)
/// - Throws: Exception if the request fails
/// - Returns: AppwriteModels.Presence<T>
/// - Returns: AppwriteModels.Presence
///
open func update<T>(
open func update(
presenceId: String,
status: String? = nil,
expiresAt: String? = nil,
metadata: Any? = nil,
permissions: [String]? = nil,
purge: Bool? = nil,
nestedType: T.Type
) async throws -> AppwriteModels.Presence<T> {
purge: Bool? = nil
) async throws -> AppwriteModels.Presence {
let apiPath: String = "/presences/{presenceId}"
.replacingOccurrences(of: "{presenceId}", with: presenceId)

Expand All @@ -242,7 +165,7 @@ open class Presences: Service {
"content-type": "application/json"
]

let converter: (Any) throws -> AppwriteModels.Presence<T> = { response in
let converter: (Any) throws -> AppwriteModels.Presence = { response in
return AppwriteModels.Presence.from(map: response as! [String: Any])
}

Expand All @@ -255,40 +178,6 @@ open class Presences: Service {
)
}

///
/// Update a presence log by its unique ID. Using the patch method you can pass
/// only specific fields that will get updated.
///
///
/// - Parameters:
/// - presenceId: String
/// - status: String (optional)
/// - expiresAt: String (optional)
/// - metadata: Any (optional)
/// - permissions: [String] (optional)
/// - purge: Bool (optional)
/// - Throws: Exception if the request fails
/// - Returns: AppwriteModels.Presence<T>
///
open func update(
presenceId: String,
status: String? = nil,
expiresAt: String? = nil,
metadata: Any? = nil,
permissions: [String]? = nil,
purge: Bool? = nil
) async throws -> AppwriteModels.Presence<[String: AnyCodable]> {
return try await update(
presenceId: presenceId,
status: status,
expiresAt: expiresAt,
metadata: metadata,
permissions: permissions,
purge: purge,
nestedType: [String: AnyCodable].self
)
}

///
/// Delete a presence log by its unique ID.
///
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

public enum Theme: String, Codable, CustomStringConvertible {
public enum BrowserTheme: String, Codable, CustomStringConvertible {
case light = "light"
case dark = "dark"

Expand Down
11 changes: 1 addition & 10 deletions Sources/AppwriteModels/Document.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,7 @@ open class Document<T : Codable>: Codable {
createdAt: map["$createdAt"] as? String ?? "",
updatedAt: map["$updatedAt"] as? String ?? "",
permissions: map["$permissions"] as? [String] ?? [],
data: try! JSONDecoder().decode(T.self, from: {
let raw = map["data"]
if let dict = raw as? [String: Any] {
return try! JSONSerialization.data(withJSONObject: dict, options: [])
} else if let raw = raw, JSONSerialization.isValidJSONObject(raw) {
return try! JSONSerialization.data(withJSONObject: raw, options: [])
} else {
return try! JSONSerialization.data(withJSONObject: [:] as [String: Any], options: [])
}
}())
data: try! JSONDecoder().decode(T.self, from: JSONSerialization.data(withJSONObject: map["data"] as? [String: Any] ?? map, options: []))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The new fallback map["data"] as? [String: Any] ?? map silently substitutes the entire parent map whenever map["data"] exists but is not a dictionary (e.g., a JSON array). In the previous code, JSONSerialization.isValidJSONObject(raw) handled that branch correctly and would still serialize the actual data value. Now, if a server ever returns an array-typed data, the entire document map (including $id, $collectionId, etc.) is passed to JSONDecoder instead — either producing a wrong T silently or crashing the try!.

Suggested change
data: try! JSONDecoder().decode(T.self, from: JSONSerialization.data(withJSONObject: map["data"] as? [String: Any] ?? map, options: []))
data: try! JSONDecoder().decode(T.self, from: {
let raw = map["data"]
if let dict = raw as? [String: Any] {
return try! JSONSerialization.data(withJSONObject: dict, options: [])
} else if let raw = raw, JSONSerialization.isValidJSONObject(raw) {
return try! JSONSerialization.data(withJSONObject: raw, options: [])
} else {
return try! JSONSerialization.data(withJSONObject: [:] as [String: Any], options: [])
}
}())

)
}
}
25 changes: 8 additions & 17 deletions Sources/AppwriteModels/Presence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation
import JSONCodable

/// Presence
open class Presence<T : Codable>: Codable {
open class Presence: Codable {

enum CodingKeys: String, CodingKey {
case id = "$id"
Expand Down Expand Up @@ -32,8 +32,8 @@ open class Presence<T : Codable>: Codable {
public let source: String
/// Presence expiry date in ISO 8601 format.
public let expiresAt: String?
/// Additional properties
public let metadata: T
/// Presence metadata.
public let metadata: [String: AnyCodable]?

init(
id: String,
Expand All @@ -44,7 +44,7 @@ open class Presence<T : Codable>: Codable {
status: String?,
source: String,
expiresAt: String?,
metadata: T
metadata: [String: AnyCodable]?
) {
self.id = id
self.createdAt = createdAt
Expand All @@ -68,7 +68,7 @@ open class Presence<T : Codable>: Codable {
self.status = try container.decodeIfPresent(String.self, forKey: .status)
self.source = try container.decode(String.self, forKey: .source)
self.expiresAt = try container.decodeIfPresent(String.self, forKey: .expiresAt)
self.metadata = try container.decode(T.self, forKey: .metadata)
self.metadata = try container.decodeIfPresent([String: AnyCodable].self, forKey: .metadata)
}

public func encode(to encoder: Encoder) throws {
Expand All @@ -82,7 +82,7 @@ open class Presence<T : Codable>: Codable {
try container.encodeIfPresent(status, forKey: .status)
try container.encode(source, forKey: .source)
try container.encodeIfPresent(expiresAt, forKey: .expiresAt)
try container.encode(metadata, forKey: .metadata)
try container.encodeIfPresent(metadata, forKey: .metadata)
}

public func toMap() -> [String: Any] {
Expand All @@ -95,7 +95,7 @@ open class Presence<T : Codable>: Codable {
"status": status as Any,
"source": source as Any,
"expiresAt": expiresAt as Any,
"metadata": (try! JSONSerialization.jsonObject(with: JSONEncoder().encode(metadata))) as? [String: Any] ?? [:]
"metadata": metadata as Any
]
}

Expand All @@ -109,16 +109,7 @@ open class Presence<T : Codable>: Codable {
status: map["status"] as? String,
source: map["source"] as! String,
expiresAt: map["expiresAt"] as? String,
metadata: try! JSONDecoder().decode(T.self, from: {
let raw = map["metadata"]
if let dict = raw as? [String: Any] {
return try! JSONSerialization.data(withJSONObject: dict, options: [])
} else if let raw = raw, JSONSerialization.isValidJSONObject(raw) {
return try! JSONSerialization.data(withJSONObject: raw, options: [])
} else {
return try! JSONSerialization.data(withJSONObject: [:] as [String: Any], options: [])
}
}())
metadata: (map["metadata"] as? [String: Any] ?? [:]).mapValues { AnyCodable($0) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 When map["metadata"] is absent or nil, the ?? [:] fallback converts it to an empty dictionary, so metadata is always non-nil on the from(map:) path. The init(from decoder:) path uses decodeIfPresent, which correctly returns nil for a missing key. Any caller checking presence.metadata != nil (or using metadata in an if let / guard let) will always see a truthy value when the object was built via from(map:), even when the server returned no metadata.

Suggested change
metadata: (map["metadata"] as? [String: Any] ?? [:]).mapValues { AnyCodable($0) }
metadata: (map["metadata"] as? [String: Any]).map { $0.mapValues { AnyCodable($0) } }

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

)
}
}
Loading