Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
5 changes: 5 additions & 0 deletions src/SDK/Language/Android.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ public function getFiles(): array
'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/ID.kt',
'template' => '/android/library/src/main/java/io/package/ID.kt.twig',
],
[
'scope' => 'default',
'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/Channel.kt',
'template' => '/android/library/src/main/java/io/package/Channel.kt.twig',
],
[
'scope' => 'default',
'destination' => '/library/src/main/java/{{ sdk.namespace | caseSlash }}/Query.kt',
Expand Down
5 changes: 5 additions & 0 deletions src/SDK/Language/Apple.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ public function getFiles(): array
'destination' => '/Sources/{{ spec.title | caseUcfirst}}/ID.swift',
'template' => 'swift/Sources/ID.swift.twig',
],
[
'scope' => 'default',
'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Channel.swift',
'template' => 'apple/Sources/Channel.swift.twig',
],
[
'scope' => 'default',
'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Query.swift',
Expand Down
10 changes: 10 additions & 0 deletions src/SDK/Language/Dart.php
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,11 @@ public function getFiles(): array
'destination' => '/lib/id.dart',
'template' => 'dart/lib/id.dart.twig',
],
[
'scope' => 'default',
'destination' => '/lib/channel.dart',
'template' => 'dart/lib/channel.dart.twig',
],
[
'scope' => 'default',
'destination' => '/lib/query.dart',
Expand Down Expand Up @@ -464,6 +469,11 @@ public function getFiles(): array
'destination' => '/test/role_test.dart',
'template' => 'dart/test/role_test.dart.twig',
],
[
'scope' => 'default',
'destination' => '/test/channel_test.dart',
'template' => 'dart/test/channel_test.dart.twig',
],
[
'scope' => 'default',
'destination' => '/test/src/enums_test.dart',
Expand Down
10 changes: 10 additions & 0 deletions src/SDK/Language/Flutter.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ public function getFiles(): array
'destination' => '/lib/id.dart',
'template' => 'dart/lib/id.dart.twig',
],
[
'scope' => 'default',
'destination' => '/lib/channel.dart',
'template' => 'dart/lib/channel.dart.twig',
],
[
'scope' => 'default',
'destination' => '/lib/query.dart',
Expand Down Expand Up @@ -290,6 +295,11 @@ public function getFiles(): array
'destination' => '/test/role_test.dart',
'template' => 'dart/test/role_test.dart.twig',
],
[
'scope' => 'default',
'destination' => '/test/channel_test.dart',
'template' => 'dart/test/channel_test.dart.twig',
],
[
'scope' => 'default',
'destination' => '/test/src/cookie_manager_test.dart',
Expand Down
5 changes: 5 additions & 0 deletions src/SDK/Language/ReactNative.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ public function getFiles(): array
'destination' => 'src/id.ts',
'template' => 'react-native/src/id.ts.twig',
],
[
'scope' => 'default',
'destination' => 'src/channel.ts',
'template' => 'react-native/src/channel.ts.twig',
],
[
'scope' => 'default',
'destination' => 'src/query.ts',
Expand Down
5 changes: 5 additions & 0 deletions src/SDK/Language/Web.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ public function getFiles(): array
'destination' => 'src/id.ts',
'template' => 'web/src/id.ts.twig',
],
[
'scope' => 'default',
'destination' => 'src/channel.ts',
'template' => 'web/src/channel.ts.twig',
],
[
'scope' => 'default',
'destination' => 'src/query.ts',
Expand Down
116 changes: 116 additions & 0 deletions templates/android/library/src/main/java/io/package/Channel.kt.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package {{ sdk.namespace | caseDot }}

/**
* Helper class to generate channel strings for realtime subscriptions.
*/
class Channel {
companion object {

/**
* Generate a database channel string.
*
* @param databaseId The database ID (default: "*")
* @param collectionId The collection ID (default: "*")
* @param documentId The document ID (default: "*")
* @param action Optional action: "create", "update", or "delete" (default: null)
* @returns The channel string
*/
fun database(databaseId: String = "*", collectionId: String = "*", documentId: String = "*", action: String? = null): String {
var channel = "databases.$databaseId.collections.$collectionId.documents.$documentId"
if (action != null) {
channel += ".$action"
}
return channel
}

/**
* Generate a tables database channel string.
*
* @param databaseId The database ID (default: "*")
* @param tableId The table ID (default: "*")
* @param rowId The row ID (default: "*")
* @param action Optional action: "create", "update", or "delete" (default: null)
* @returns The channel string
*/
fun tablesdb(databaseId: String = "*", tableId: String = "*", rowId: String = "*", action: String? = null): String {
var channel = "tablesdb.$databaseId.tables.$tableId.rows.$rowId"
if (action != null) {
channel += ".$action"
}
return channel
}

/**
* Generate an account channel string.
*
* @param userId The user ID (default: "*")
* @returns The channel string
*/
fun account(userId: String = "*"): String {
return "account.$userId"
}

/**
* Generate a files channel string.
*
* @param bucketId The bucket ID (default: "*")
* @param fileId The file ID (default: "*")
* @param action Optional action: "create", "update", or "delete" (default: null)
* @returns The channel string
*/
fun files(bucketId: String = "*", fileId: String = "*", action: String? = null): String {
var channel = "buckets.$bucketId.files.$fileId"
if (action != null) {
channel += ".$action"
}
return channel
}

/**
* Generate an executions channel string.
*
* @param functionId The function ID (default: "*")
* @param executionId The execution ID (default: "*")
* @param action Optional action: "create", "update", or "delete" (default: null)
* @returns The channel string
*/
fun executions(functionId: String = "*", executionId: String = "*", action: String? = null): String {
var channel = "functions.$functionId.executions.$executionId"
if (action != null) {
channel += ".$action"
}
return channel
}

/**
* Generate a teams channel string.
*
* @param teamId The team ID (default: "*")
* @param action Optional action: "create", "update", or "delete" (default: null)
* @returns The channel string
*/
fun teams(teamId: String = "*", action: String? = null): String {
var channel = "teams.$teamId"
if (action != null) {
channel += ".$action"
}
return channel
}

/**
* Generate a memberships channel string.
*
* @param membershipId The membership ID (default: "*")
* @param action Optional action: "create", "update", or "delete" (default: null)
* @returns The channel string
*/
fun memberships(membershipId: String = "*", action: String? = null): String {
var channel = "memberships.$membershipId"
if (action != null) {
channel += ".$action"
}
return channel
}
}
}

108 changes: 108 additions & 0 deletions templates/apple/Sources/Channel.swift.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
public class Channel {
/**
* Generate a database channel string.
*
* @param databaseId The database ID (default: "*")
* @param collectionId The collection ID (default: "*")
* @param documentId The document ID (default: "*")
* @param action Optional action: "create", "update", or "delete" (default: nil)
* @returns The channel string
*/
public static func database(databaseId: String = "*", collectionId: String = "*", documentId: String = "*", action: String? = nil) -> String {
var channel = "databases.\(databaseId).collections.\(collectionId).documents.\(documentId)"
if let action = action {
channel += ".\(action)"
}
return channel
}
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 18, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for ID validation or sanitization in the template codebase
fd -e twig . -type f | head -20

Repository: appwrite/sdk-generator

Length of output: 236


🏁 Script executed:

# Check for any ID format validation patterns in templates
rg -i "alphanumeric|valid.*id|id.*constraint" --type-list | head -5

Repository: appwrite/sdk-generator

Length of output: 455


🌐 Web query:

Appwrite ID format constraints valid characters specifications

💡 Result:

Summary of Appwrite ID format rules:

  • Allowed characters: lowercase letters a–z, uppercase A–Z, digits 0–9, period (.), hyphen (-), underscore (_). [1]
  • Cannot start with a special character (., -, _). [1]
  • Maximum length: 36 characters. [1]
  • You can provide a custom ID or use the SDK helper ID.unique() / pass "unique()" when using REST. [2]

Sources: [1] Appwrite Users API reference; [2] Appwrite REST / Unique ID documentation.


🏁 Script executed:

# First, find and read the Channel.swift.twig file
fd "Channel.swift.twig" -type f

Repository: appwrite/sdk-generator

Length of output: 236


🏁 Script executed:

# Search for any sanitization, validation, or escaping logic in template files
rg "sanitize|escape|validate|replace.*\\\." -i templates/

Repository: appwrite/sdk-generator

Length of output: 50378


🏁 Script executed:

# Check other language channel implementations for comparison
fd "Channel.*twig" -type f | head -10

Repository: appwrite/sdk-generator

Length of output: 236


🏁 Script executed:

# Find the Channel.swift.twig file
fd "Channel.swift.twig"

Repository: appwrite/sdk-generator

Length of output: 108


🏁 Script executed:

# Read the Channel.swift.twig file
cat templates/apple/Sources/Channel.swift.twig

Repository: appwrite/sdk-generator

Length of output: 3893


🏁 Script executed:

# Search for any ID validation or escaping in Swift templates
rg "sanitize|escape|validate" templates/apple/ -i

Repository: appwrite/sdk-generator

Length of output: 892


Add ID validation or escaping to prevent malformed channel strings.

Appwrite allows IDs to contain periods, hyphens, and underscores. The methods currently interpolate these IDs directly into dot-separated channel strings without validation or escaping. If an ID contains a period (e.g., databaseId="my.db"), it produces an ambiguous channel string like "databases.my.db.collections.*..." that cannot be reliably parsed. Either validate that IDs don't contain delimiter characters, or escape/encode IDs before interpolation.

Also applies to: 30-36 (tablesdb), 44-46 (account), 56-62 (files), 72-78 (executions), 87-93 (teams), 102-108 (memberships)

🤖 Prompt for AI Agents
In templates/apple/Sources/Channel.swift.twig around lines 13 to 19 (and
similarly for lines 30-36, 44-46, 56-62, 72-78, 87-93, 102-108), the code
interpolates IDs directly into dot-delimited channel strings which breaks when
IDs contain delimiter characters (like periods). Fix by validating or
normalizing IDs before interpolation: either reject IDs containing delimiters
and throw/return an error, or consistently escape/encode IDs (e.g.,
percent-encode or a reversible encoding) and use the encoded form in the channel
string so parsing remains unambiguous; apply the same validation/encoding logic
to all listed channel factory functions and update their tests accordingly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We dont follow this in sdk. It is just an helper similar to Query

Copy link
Contributor

Choose a reason for hiding this comment

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

@ArnabChatterjee20k, thank you for the clarification! I understand that the Channel helper follows the same pattern as Query and other helpers in the SDK, where IDs are accepted as-is without validation or escaping. This is consistent with your SDK's design philosophy of providing lightweight helper utilities.

I've noted this pattern for future reviews.


✏️ Learnings added
Learnt from: ArnabChatterjee20k
Repo: appwrite/sdk-generator PR: 1266
File: templates/apple/Sources/Channel.swift.twig:13-19
Timestamp: 2025-12-18T18:43:30.593Z
Learning: In the appwrite/sdk-generator repository, helper classes like Channel and Query accept IDs without validation or escaping. These are lightweight helper utilities that construct strings directly from provided parameters without sanitizing or encoding them. This is an intentional design pattern across the SDK.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: ArnabChatterjee20k
Repo: appwrite/sdk-generator PR: 1266
File: templates/swift/Sources/WebSockets/WebSocketClient.swift.twig:140-140
Timestamp: 2025-12-18T18:42:14.446Z
Learning: In templates/swift/Sources/WebSockets/WebSocketClient.swift.twig, the Channel type from NIOCore is explicitly qualified as `NIOCore.Channel` to avoid naming conflicts with the new Channel helper class for realtime subscriptions. This is not a breaking change - it's the same type, just explicitly qualified in the source.


/**
* Generate a tables database channel string.
*
* @param databaseId The database ID (default: "*")
* @param tableId The table ID (default: "*")
* @param rowId The row ID (default: "*")
* @param action Optional action: "create", "update", or "delete" (default: nil)
* @returns The channel string
*/
public static func tablesdb(databaseId: String = "*", tableId: String = "*", rowId: String = "*", action: String? = nil) -> String {
var channel = "tablesdb.\(databaseId).tables.\(tableId).rows.\(rowId)"
if let action = action {
channel += ".\(action)"
}
return channel
}

/**
* Generate an account channel string.
*
* @param userId The user ID (default: "*")
* @returns The channel string
*/
public static func account(userId: String = "*") -> String {
return "account.\(userId)"
}

/**
* Generate a files channel string.
*
* @param bucketId The bucket ID (default: "*")
* @param fileId The file ID (default: "*")
* @param action Optional action: "create", "update", or "delete" (default: nil)
* @returns The channel string
*/
public static func files(bucketId: String = "*", fileId: String = "*", action: String? = nil) -> String {
var channel = "buckets.\(bucketId).files.\(fileId)"
if let action = action {
channel += ".\(action)"
}
return channel
}

/**
* Generate an executions channel string.
*
* @param functionId The function ID (default: "*")
* @param executionId The execution ID (default: "*")
* @param action Optional action: "create", "update", or "delete" (default: nil)
* @returns The channel string
*/
public static func executions(functionId: String = "*", executionId: String = "*", action: String? = nil) -> String {
var channel = "functions.\(functionId).executions.\(executionId)"
if let action = action {
channel += ".\(action)"
}
return channel
}

/**
* Generate a teams channel string.
*
* @param teamId The team ID (default: "*")
* @param action Optional action: "create", "update", or "delete" (default: nil)
* @returns The channel string
*/
public static func teams(teamId: String = "*", action: String? = nil) -> String {
var channel = "teams.\(teamId)"
if let action = action {
channel += ".\(action)"
}
return channel
}

/**
* Generate a memberships channel string.
*
* @param membershipId The membership ID (default: "*")
* @param action Optional action: "create", "update", or "delete" (default: nil)
* @returns The channel string
*/
public static func memberships(membershipId: String = "*", action: String? = nil) -> String {
var channel = "memberships.\(membershipId)"
if let action = action {
channel += ".\(action)"
}
return channel
}
}

Loading
Loading