From 29471d18af97c4e0f53d62047e5229707c754b78 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 18 Dec 2025 10:39:54 +0530 Subject: [PATCH 01/26] added channel helpers for realtime in the client sdks --- src/SDK/Language/Android.php | 5 + src/SDK/Language/Apple.php | 5 + src/SDK/Language/Dart.php | 5 + src/SDK/Language/Flutter.php | 10 ++ src/SDK/Language/ReactNative.php | 5 + src/SDK/Language/Web.php | 5 + .../src/main/java/io/package/Channel.kt.twig | 116 ++++++++++++++++++ templates/dart/lib/channel.dart.twig | 116 ++++++++++++++++++ templates/dart/lib/package.dart.twig | 1 + templates/dart/test/channel_test.dart.twig | 112 +++++++++++++++++ templates/react-native/src/channel.ts.twig | 111 +++++++++++++++++ templates/react-native/src/index.ts.twig | 1 + templates/swift/Sources/Channel.swift.twig | 108 ++++++++++++++++ templates/web/src/channel.ts.twig | 111 +++++++++++++++++ templates/web/src/index.ts.twig | 1 + tests/languages/android/Tests.kt | 22 ++++ tests/languages/apple/Tests.swift | 22 ++++ tests/languages/flutter/tests.dart | 22 ++++ tests/languages/node/test.js | 22 ++++ tests/languages/swift/Tests.swift | 22 ++++ tests/languages/web/index.html | 22 ++++ tests/languages/web/node.js | 22 ++++ 22 files changed, 866 insertions(+) create mode 100644 templates/android/library/src/main/java/io/package/Channel.kt.twig create mode 100644 templates/dart/lib/channel.dart.twig create mode 100644 templates/dart/test/channel_test.dart.twig create mode 100644 templates/react-native/src/channel.ts.twig create mode 100644 templates/swift/Sources/Channel.swift.twig create mode 100644 templates/web/src/channel.ts.twig diff --git a/src/SDK/Language/Android.php b/src/SDK/Language/Android.php index 19a22f01a3..64242bd123 100644 --- a/src/SDK/Language/Android.php +++ b/src/SDK/Language/Android.php @@ -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', diff --git a/src/SDK/Language/Apple.php b/src/SDK/Language/Apple.php index 4313f9d34a..3f409b5936 100644 --- a/src/SDK/Language/Apple.php +++ b/src/SDK/Language/Apple.php @@ -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' => 'swift/Sources/Channel.swift.twig', + ], [ 'scope' => 'default', 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Query.swift', diff --git a/src/SDK/Language/Dart.php b/src/SDK/Language/Dart.php index 6ad8efbaa2..01e1975099 100644 --- a/src/SDK/Language/Dart.php +++ b/src/SDK/Language/Dart.php @@ -464,6 +464,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', diff --git a/src/SDK/Language/Flutter.php b/src/SDK/Language/Flutter.php index ab0c5ec3a5..613720c094 100644 --- a/src/SDK/Language/Flutter.php +++ b/src/SDK/Language/Flutter.php @@ -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', @@ -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', diff --git a/src/SDK/Language/ReactNative.php b/src/SDK/Language/ReactNative.php index 81748709cb..f0ccd54c22 100644 --- a/src/SDK/Language/ReactNative.php +++ b/src/SDK/Language/ReactNative.php @@ -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', diff --git a/src/SDK/Language/Web.php b/src/SDK/Language/Web.php index 2490f833f9..4a94da3a9d 100644 --- a/src/SDK/Language/Web.php +++ b/src/SDK/Language/Web.php @@ -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', diff --git a/templates/android/library/src/main/java/io/package/Channel.kt.twig b/templates/android/library/src/main/java/io/package/Channel.kt.twig new file mode 100644 index 0000000000..10f9fcd410 --- /dev/null +++ b/templates/android/library/src/main/java/io/package/Channel.kt.twig @@ -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 + } + } +} + diff --git a/templates/dart/lib/channel.dart.twig b/templates/dart/lib/channel.dart.twig new file mode 100644 index 0000000000..7966dfacb3 --- /dev/null +++ b/templates/dart/lib/channel.dart.twig @@ -0,0 +1,116 @@ +part of '{{ language.params.packageName }}.dart'; + +/// Helper class to generate channel strings for realtime subscriptions. +class Channel { + Channel._(); + + /// Generate a database channel string. + /// + /// [databaseId] The database ID (default: '*') + /// [collectionId] The collection ID (default: '*') + /// [documentId] The document ID (default: '*') + /// [action] Optional action: 'create', 'update', or 'delete' (default: null) + static String database({ + String databaseId = '*', + String collectionId = '*', + String documentId = '*', + String? action, + }) { + String channel = 'databases.$databaseId.collections.$collectionId.documents.$documentId'; + if (action != null) { + channel += '.$action'; + } + return channel; + } + + /// Generate a tables database channel string. + /// + /// [databaseId] The database ID (default: '*') + /// [tableId] The table ID (default: '*') + /// [rowId] The row ID (default: '*') + /// [action] Optional action: 'create', 'update', or 'delete' (default: null) + static String tablesdb({ + String databaseId = '*', + String tableId = '*', + String rowId = '*', + String? action, + }) { + String channel = 'tablesdb.$databaseId.tables.$tableId.rows.$rowId'; + if (action != null) { + channel += '.$action'; + } + return channel; + } + + /// Generate an account channel string. + /// + /// [userId] The user ID (default: '*') + static String account({String userId = '*'}) { + return 'account.$userId'; + } + + /// Generate a files channel string. + /// + /// [bucketId] The bucket ID (default: '*') + /// [fileId] The file ID (default: '*') + /// [action] Optional action: 'create', 'update', or 'delete' (default: null) + static String files({ + String bucketId = '*', + String fileId = '*', + String? action, + }) { + String channel = 'buckets.$bucketId.files.$fileId'; + if (action != null) { + channel += '.$action'; + } + return channel; + } + + /// Generate an executions channel string. + /// + /// [functionId] The function ID (default: '*') + /// [executionId] The execution ID (default: '*') + /// [action] Optional action: 'create', 'update', or 'delete' (default: null) + static String executions({ + String functionId = '*', + String executionId = '*', + String? action, + }) { + String channel = 'functions.$functionId.executions.$executionId'; + if (action != null) { + channel += '.$action'; + } + return channel; + } + + /// Generate a teams channel string. + /// + /// [teamId] The team ID (default: '*') + /// [action] Optional action: 'create', 'update', or 'delete' (default: null) + static String teams({ + String teamId = '*', + String? action, + }) { + String channel = 'teams.$teamId'; + if (action != null) { + channel += '.$action'; + } + return channel; + } + + /// Generate a memberships channel string. + /// + /// [membershipId] The membership ID (default: '*') + /// [action] Optional action: 'create', 'update', or 'delete' (default: null) + static String memberships({ + String membershipId = '*', + String? action, + }) { + String channel = 'memberships.$membershipId'; + if (action != null) { + channel += '.$action'; + } + return channel; + } +} + diff --git a/templates/dart/lib/package.dart.twig b/templates/dart/lib/package.dart.twig index bca6862a36..43e4a2c761 100644 --- a/templates/dart/lib/package.dart.twig +++ b/templates/dart/lib/package.dart.twig @@ -27,6 +27,7 @@ part 'query.dart'; part 'permission.dart'; part 'role.dart'; part 'id.dart'; +part 'channel.dart'; part 'operator.dart'; {% for service in spec.services %} part 'services/{{service.name | caseSnake}}.dart'; diff --git a/templates/dart/test/channel_test.dart.twig b/templates/dart/test/channel_test.dart.twig new file mode 100644 index 0000000000..2a3dd59535 --- /dev/null +++ b/templates/dart/test/channel_test.dart.twig @@ -0,0 +1,112 @@ +import 'package:{{ language.params.packageName }}/{{ language.params.packageName }}.dart'; +{% if 'dart' in language.params.packageName %} +import 'package:test/test.dart'; +{% else %} +import 'package:flutter_test/flutter_test.dart'; +{% endif %} + +void main() { + group('database()', () { + test('returns database channel with defaults', () { + expect(Channel.database(), 'databases.*.collections.*.documents.*'); + }); + + test('returns database channel with specific IDs', () { + expect(Channel.database(databaseId: 'db1', collectionId: 'col1', documentId: 'doc1'), + 'databases.db1.collections.col1.documents.doc1'); + }); + + test('returns database channel with action', () { + expect(Channel.database(databaseId: 'db1', collectionId: 'col1', documentId: 'doc1', action: 'create'), + 'databases.db1.collections.col1.documents.doc1.create'); + }); + }); + + group('tablesdb()', () { + test('returns tablesdb channel with defaults', () { + expect(Channel.tablesdb(), 'tablesdb.*.tables.*.rows.*'); + }); + + test('returns tablesdb channel with specific IDs', () { + expect(Channel.tablesdb(databaseId: 'db1', tableId: 'table1', rowId: 'row1'), + 'tablesdb.db1.tables.table1.rows.row1'); + }); + + test('returns tablesdb channel with action', () { + expect(Channel.tablesdb(databaseId: 'db1', tableId: 'table1', rowId: 'row1', action: 'update'), + 'tablesdb.db1.tables.table1.rows.row1.update'); + }); + }); + + group('account()', () { + test('returns account channel with default', () { + expect(Channel.account(), 'account.*'); + }); + + test('returns account channel with specific user ID', () { + expect(Channel.account(userId: 'user123'), 'account.user123'); + }); + }); + + group('files()', () { + test('returns files channel with defaults', () { + expect(Channel.files(), 'buckets.*.files.*'); + }); + + test('returns files channel with specific IDs', () { + expect(Channel.files(bucketId: 'bucket1', fileId: 'file1'), + 'buckets.bucket1.files.file1'); + }); + + test('returns files channel with action', () { + expect(Channel.files(bucketId: 'bucket1', fileId: 'file1', action: 'delete'), + 'buckets.bucket1.files.file1.delete'); + }); + }); + + group('executions()', () { + test('returns executions channel with defaults', () { + expect(Channel.executions(), 'functions.*.executions.*'); + }); + + test('returns executions channel with specific IDs', () { + expect(Channel.executions(functionId: 'func1', executionId: 'exec1'), + 'functions.func1.executions.exec1'); + }); + + test('returns executions channel with action', () { + expect(Channel.executions(functionId: 'func1', executionId: 'exec1', action: 'create'), + 'functions.func1.executions.exec1.create'); + }); + }); + + group('teams()', () { + test('returns teams channel with default', () { + expect(Channel.teams(), 'teams.*'); + }); + + test('returns teams channel with specific team ID', () { + expect(Channel.teams(teamId: 'team1'), 'teams.team1'); + }); + + test('returns teams channel with action', () { + expect(Channel.teams(teamId: 'team1', action: 'create'), 'teams.team1.create'); + }); + }); + + group('memberships()', () { + test('returns memberships channel with default', () { + expect(Channel.memberships(), 'memberships.*'); + }); + + test('returns memberships channel with specific membership ID', () { + expect(Channel.memberships(membershipId: 'membership1'), 'memberships.membership1'); + }); + + test('returns memberships channel with action', () { + expect(Channel.memberships(membershipId: 'membership1', action: 'update'), + 'memberships.membership1.update'); + }); + }); +} + diff --git a/templates/react-native/src/channel.ts.twig b/templates/react-native/src/channel.ts.twig new file mode 100644 index 0000000000..fe2cada9e6 --- /dev/null +++ b/templates/react-native/src/channel.ts.twig @@ -0,0 +1,111 @@ +/** + * Helper class to generate channel strings for realtime subscriptions. + */ +export class Channel { + /** + * Generate a database channel string. + * + * @param {string} databaseId + * @param {string} collectionId + * @param {string} documentId + * @param {'create'|'update'|'delete'|null} action + * @returns {string} + */ + static database(databaseId: string = '*', collectionId: string = '*', documentId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { + let channel = `databases.${databaseId}.collections.${collectionId}.documents.${documentId}`; + if (action) { + channel += `.${action}`; + } + return channel; + } + + /** + * Generate a tables database channel string. + * + * @param {string} databaseId + * @param {string} tableId + * @param {string} rowId + * @param {'create'|'update'|'delete'|null} action + * @returns {string} + */ + static tablesdb(databaseId: string = '*', tableId: string = '*', rowId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { + let channel = `tablesdb.${databaseId}.tables.${tableId}.rows.${rowId}`; + if (action) { + channel += `.${action}`; + } + return channel; + } + + /** + * Generate an account channel string. + * + * @param {string} userId + * @returns {string} + */ + static account(userId: string = '*'): string { + return `account.${userId}`; + } + + /** + * Generate a files channel string. + * + * @param {string} bucketId + * @param {string} fileId + * @param {'create'|'update'|'delete'|null} action + * @returns {string} + */ + static files(bucketId: string = '*', fileId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { + let channel = `buckets.${bucketId}.files.${fileId}`; + if (action) { + channel += `.${action}`; + } + return channel; + } + + /** + * Generate an executions channel string. + * + * @param {string} functionId + * @param {string} executionId + * @param {'create'|'update'|'delete'|null} action + * @returns {string} + */ + static executions(functionId: string = '*', executionId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { + let channel = `functions.${functionId}.executions.${executionId}`; + if (action) { + channel += `.${action}`; + } + return channel; + } + + /** + * Generate a teams channel string. + * + * @param {string} teamId + * @param {'create'|'update'|'delete'|null} action + * @returns {string} + */ + static teams(teamId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { + let channel = `teams.${teamId}`; + if (action) { + channel += `.${action}`; + } + return channel; + } + + /** + * Generate a memberships channel string. + * + * @param {string} membershipId + * @param {'create'|'update'|'delete'|null} action + * @returns {string} + */ + static memberships(membershipId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { + let channel = `memberships.${membershipId}`; + if (action) { + channel += `.${action}`; + } + return channel; + } +} + diff --git a/templates/react-native/src/index.ts.twig b/templates/react-native/src/index.ts.twig index 8a9b5aa947..e27f296499 100644 --- a/templates/react-native/src/index.ts.twig +++ b/templates/react-native/src/index.ts.twig @@ -8,6 +8,7 @@ export { Query } from './query'; export { Permission } from './permission'; export { Role } from './role'; export { ID } from './id'; +export { Channel } from './channel'; export { Operator, Condition } from './operator'; {% for enum in spec.allEnums %} export { {{ enum.name | caseUcfirst }} } from './enums/{{enum.name | caseKebab}}'; diff --git a/templates/swift/Sources/Channel.swift.twig b/templates/swift/Sources/Channel.swift.twig new file mode 100644 index 0000000000..ba14f2f20b --- /dev/null +++ b/templates/swift/Sources/Channel.swift.twig @@ -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 + } + + /** + * 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 + } +} + diff --git a/templates/web/src/channel.ts.twig b/templates/web/src/channel.ts.twig new file mode 100644 index 0000000000..fe2cada9e6 --- /dev/null +++ b/templates/web/src/channel.ts.twig @@ -0,0 +1,111 @@ +/** + * Helper class to generate channel strings for realtime subscriptions. + */ +export class Channel { + /** + * Generate a database channel string. + * + * @param {string} databaseId + * @param {string} collectionId + * @param {string} documentId + * @param {'create'|'update'|'delete'|null} action + * @returns {string} + */ + static database(databaseId: string = '*', collectionId: string = '*', documentId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { + let channel = `databases.${databaseId}.collections.${collectionId}.documents.${documentId}`; + if (action) { + channel += `.${action}`; + } + return channel; + } + + /** + * Generate a tables database channel string. + * + * @param {string} databaseId + * @param {string} tableId + * @param {string} rowId + * @param {'create'|'update'|'delete'|null} action + * @returns {string} + */ + static tablesdb(databaseId: string = '*', tableId: string = '*', rowId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { + let channel = `tablesdb.${databaseId}.tables.${tableId}.rows.${rowId}`; + if (action) { + channel += `.${action}`; + } + return channel; + } + + /** + * Generate an account channel string. + * + * @param {string} userId + * @returns {string} + */ + static account(userId: string = '*'): string { + return `account.${userId}`; + } + + /** + * Generate a files channel string. + * + * @param {string} bucketId + * @param {string} fileId + * @param {'create'|'update'|'delete'|null} action + * @returns {string} + */ + static files(bucketId: string = '*', fileId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { + let channel = `buckets.${bucketId}.files.${fileId}`; + if (action) { + channel += `.${action}`; + } + return channel; + } + + /** + * Generate an executions channel string. + * + * @param {string} functionId + * @param {string} executionId + * @param {'create'|'update'|'delete'|null} action + * @returns {string} + */ + static executions(functionId: string = '*', executionId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { + let channel = `functions.${functionId}.executions.${executionId}`; + if (action) { + channel += `.${action}`; + } + return channel; + } + + /** + * Generate a teams channel string. + * + * @param {string} teamId + * @param {'create'|'update'|'delete'|null} action + * @returns {string} + */ + static teams(teamId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { + let channel = `teams.${teamId}`; + if (action) { + channel += `.${action}`; + } + return channel; + } + + /** + * Generate a memberships channel string. + * + * @param {string} membershipId + * @param {'create'|'update'|'delete'|null} action + * @returns {string} + */ + static memberships(membershipId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { + let channel = `memberships.${membershipId}`; + if (action) { + channel += `.${action}`; + } + return channel; + } +} + diff --git a/templates/web/src/index.ts.twig b/templates/web/src/index.ts.twig index c9aba90fc7..54ad22739d 100644 --- a/templates/web/src/index.ts.twig +++ b/templates/web/src/index.ts.twig @@ -15,6 +15,7 @@ export type { QueryTypes, QueryTypesList } from './query'; export { Permission } from './permission'; export { Role } from './role'; export { ID } from './id'; +export { Channel } from './channel'; export { Operator, Condition } from './operator'; {% for enum in spec.allEnums %} export { {{ enum.name | caseUcfirst }} } from './enums/{{enum.name | caseKebab}}'; diff --git a/tests/languages/android/Tests.kt b/tests/languages/android/Tests.kt index aa8a083ef3..eaed5dd53d 100644 --- a/tests/languages/android/Tests.kt +++ b/tests/languages/android/Tests.kt @@ -269,6 +269,28 @@ class ServiceTest { writeToFile(ID.unique()) writeToFile(ID.custom("custom_id")) + // Channel helper tests + writeToFile(Channel.database()) + writeToFile(Channel.database("db1", "col1", "doc1")) + writeToFile(Channel.database("db1", "col1", "doc1", "create")) + writeToFile(Channel.tablesdb()) + writeToFile(Channel.tablesdb("db1", "table1", "row1")) + writeToFile(Channel.tablesdb("db1", "table1", "row1", "update")) + writeToFile(Channel.account()) + writeToFile(Channel.account("user123")) + writeToFile(Channel.files()) + writeToFile(Channel.files("bucket1", "file1")) + writeToFile(Channel.files("bucket1", "file1", "delete")) + writeToFile(Channel.executions()) + writeToFile(Channel.executions("func1", "exec1")) + writeToFile(Channel.executions("func1", "exec1", "create")) + writeToFile(Channel.teams()) + writeToFile(Channel.teams("team1")) + writeToFile(Channel.teams("team1", "create")) + writeToFile(Channel.memberships()) + writeToFile(Channel.memberships("membership1")) + writeToFile(Channel.memberships("membership1", "update")) + // Operator helper tests writeToFile(Operator.increment(1)) writeToFile(Operator.increment(5, 100)) diff --git a/tests/languages/apple/Tests.swift b/tests/languages/apple/Tests.swift index ae8b55b921..8917ce1b2c 100644 --- a/tests/languages/apple/Tests.swift +++ b/tests/languages/apple/Tests.swift @@ -248,6 +248,28 @@ class Tests: XCTestCase { print(ID.unique()) print(ID.custom("custom_id")) + // Channel helper tests + print(Channel.database()) + print(Channel.database(databaseId: "db1", collectionId: "col1", documentId: "doc1")) + print(Channel.database(databaseId: "db1", collectionId: "col1", documentId: "doc1", action: "create")) + print(Channel.tablesdb()) + print(Channel.tablesdb(databaseId: "db1", tableId: "table1", rowId: "row1")) + print(Channel.tablesdb(databaseId: "db1", tableId: "table1", rowId: "row1", action: "update")) + print(Channel.account()) + print(Channel.account(userId: "user123")) + print(Channel.files()) + print(Channel.files(bucketId: "bucket1", fileId: "file1")) + print(Channel.files(bucketId: "bucket1", fileId: "file1", action: "delete")) + print(Channel.executions()) + print(Channel.executions(functionId: "func1", executionId: "exec1")) + print(Channel.executions(functionId: "func1", executionId: "exec1", action: "create")) + print(Channel.teams()) + print(Channel.teams(teamId: "team1")) + print(Channel.teams(teamId: "team1", action: "create")) + print(Channel.memberships()) + print(Channel.memberships(membershipId: "membership1")) + print(Channel.memberships(membershipId: "membership1", action: "update")) + // Operator helper tests print(Operator.increment(1)) print(Operator.increment(5, max: 100)) diff --git a/tests/languages/flutter/tests.dart b/tests/languages/flutter/tests.dart index 939c0ff148..c6ab56563f 100644 --- a/tests/languages/flutter/tests.dart +++ b/tests/languages/flutter/tests.dart @@ -247,6 +247,28 @@ void main() async { print(ID.unique()); print(ID.custom('custom_id')); + // Channel helper tests + print(Channel.database()); + print(Channel.database(databaseId: 'db1', collectionId: 'col1', documentId: 'doc1')); + print(Channel.database(databaseId: 'db1', collectionId: 'col1', documentId: 'doc1', action: 'create')); + print(Channel.tablesdb()); + print(Channel.tablesdb(databaseId: 'db1', tableId: 'table1', rowId: 'row1')); + print(Channel.tablesdb(databaseId: 'db1', tableId: 'table1', rowId: 'row1', action: 'update')); + print(Channel.account()); + print(Channel.account(userId: 'user123')); + print(Channel.files()); + print(Channel.files(bucketId: 'bucket1', fileId: 'file1')); + print(Channel.files(bucketId: 'bucket1', fileId: 'file1', action: 'delete')); + print(Channel.executions()); + print(Channel.executions(functionId: 'func1', executionId: 'exec1')); + print(Channel.executions(functionId: 'func1', executionId: 'exec1', action: 'create')); + print(Channel.teams()); + print(Channel.teams(teamId: 'team1')); + print(Channel.teams(teamId: 'team1', action: 'create')); + print(Channel.memberships()); + print(Channel.memberships(membershipId: 'membership1')); + print(Channel.memberships(membershipId: 'membership1', action: 'update')); + // Operator helper tests print(Operator.increment(1)); print(Operator.increment(5, 100)); diff --git a/tests/languages/node/test.js b/tests/languages/node/test.js index ee0fd703f5..c52aafd28b 100644 --- a/tests/languages/node/test.js +++ b/tests/languages/node/test.js @@ -326,6 +326,28 @@ async function start() { console.log(ID.unique()); console.log(ID.custom('custom_id')); + // Channel helper tests + console.log(Channel.database()); + console.log(Channel.database('db1', 'col1', 'doc1')); + console.log(Channel.database('db1', 'col1', 'doc1', 'create')); + console.log(Channel.tablesdb()); + console.log(Channel.tablesdb('db1', 'table1', 'row1')); + console.log(Channel.tablesdb('db1', 'table1', 'row1', 'update')); + console.log(Channel.account()); + console.log(Channel.account('user123')); + console.log(Channel.files()); + console.log(Channel.files('bucket1', 'file1')); + console.log(Channel.files('bucket1', 'file1', 'delete')); + console.log(Channel.executions()); + console.log(Channel.executions('func1', 'exec1')); + console.log(Channel.executions('func1', 'exec1', 'create')); + console.log(Channel.teams()); + console.log(Channel.teams('team1')); + console.log(Channel.teams('team1', 'create')); + console.log(Channel.memberships()); + console.log(Channel.memberships('membership1')); + console.log(Channel.memberships('membership1', 'update')); + // Operator helper tests console.log(Operator.increment(1)); console.log(Operator.increment(5, 100)); diff --git a/tests/languages/swift/Tests.swift b/tests/languages/swift/Tests.swift index 8719dc6466..d682ff60de 100644 --- a/tests/languages/swift/Tests.swift +++ b/tests/languages/swift/Tests.swift @@ -235,6 +235,28 @@ class Tests: XCTestCase { print(ID.unique()) print(ID.custom("custom_id")) + // Channel helper tests + print(Channel.database()) + print(Channel.database(databaseId: "db1", collectionId: "col1", documentId: "doc1")) + print(Channel.database(databaseId: "db1", collectionId: "col1", documentId: "doc1", action: "create")) + print(Channel.tablesdb()) + print(Channel.tablesdb(databaseId: "db1", tableId: "table1", rowId: "row1")) + print(Channel.tablesdb(databaseId: "db1", tableId: "table1", rowId: "row1", action: "update")) + print(Channel.account()) + print(Channel.account(userId: "user123")) + print(Channel.files()) + print(Channel.files(bucketId: "bucket1", fileId: "file1")) + print(Channel.files(bucketId: "bucket1", fileId: "file1", action: "delete")) + print(Channel.executions()) + print(Channel.executions(functionId: "func1", executionId: "exec1")) + print(Channel.executions(functionId: "func1", executionId: "exec1", action: "create")) + print(Channel.teams()) + print(Channel.teams(teamId: "team1")) + print(Channel.teams(teamId: "team1", action: "create")) + print(Channel.memberships()) + print(Channel.memberships(membershipId: "membership1")) + print(Channel.memberships(membershipId: "membership1", action: "update")) + // Operator helper tests print(Operator.increment(1)) print(Operator.increment(5, max: 100)) diff --git a/tests/languages/web/index.html b/tests/languages/web/index.html index 7e81de9c5e..8aced2b342 100644 --- a/tests/languages/web/index.html +++ b/tests/languages/web/index.html @@ -319,6 +319,28 @@ console.log(ID.unique()); console.log(ID.custom('custom_id')); + // Channel helper tests + console.log(Channel.database()); + console.log(Channel.database('db1', 'col1', 'doc1')); + console.log(Channel.database('db1', 'col1', 'doc1', 'create')); + console.log(Channel.tablesdb()); + console.log(Channel.tablesdb('db1', 'table1', 'row1')); + console.log(Channel.tablesdb('db1', 'table1', 'row1', 'update')); + console.log(Channel.account()); + console.log(Channel.account('user123')); + console.log(Channel.files()); + console.log(Channel.files('bucket1', 'file1')); + console.log(Channel.files('bucket1', 'file1', 'delete')); + console.log(Channel.executions()); + console.log(Channel.executions('func1', 'exec1')); + console.log(Channel.executions('func1', 'exec1', 'create')); + console.log(Channel.teams()); + console.log(Channel.teams('team1')); + console.log(Channel.teams('team1', 'create')); + console.log(Channel.memberships()); + console.log(Channel.memberships('membership1')); + console.log(Channel.memberships('membership1', 'update')); + // Operator helper tests console.log(Operator.increment(1)); console.log(Operator.increment(5, 100)); diff --git a/tests/languages/web/node.js b/tests/languages/web/node.js index f4c7bed43a..8e82b34781 100644 --- a/tests/languages/web/node.js +++ b/tests/languages/web/node.js @@ -250,6 +250,28 @@ async function start() { console.log(ID.unique()); console.log(ID.custom('custom_id')); + // Channel helper tests + console.log(Channel.database()); + console.log(Channel.database('db1', 'col1', 'doc1')); + console.log(Channel.database('db1', 'col1', 'doc1', 'create')); + console.log(Channel.tablesdb()); + console.log(Channel.tablesdb('db1', 'table1', 'row1')); + console.log(Channel.tablesdb('db1', 'table1', 'row1', 'update')); + console.log(Channel.account()); + console.log(Channel.account('user123')); + console.log(Channel.files()); + console.log(Channel.files('bucket1', 'file1')); + console.log(Channel.files('bucket1', 'file1', 'delete')); + console.log(Channel.executions()); + console.log(Channel.executions('func1', 'exec1')); + console.log(Channel.executions('func1', 'exec1', 'create')); + console.log(Channel.teams()); + console.log(Channel.teams('team1')); + console.log(Channel.teams('team1', 'create')); + console.log(Channel.memberships()); + console.log(Channel.memberships('membership1')); + console.log(Channel.memberships('membership1', 'update')); + // Operator helper tests console.log(Operator.increment(1)); console.log(Operator.increment(5, 100)); From 3f13cb1a000ab5311993be949cdbfccb10b04c16 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 18 Dec 2025 23:24:02 +0530 Subject: [PATCH 02/26] Add Channel support to SDKs and templates - Updated Apple and Dart SDKs to include Channel templates. - Added Channel class in Swift template for Apple SDK. - Included Channel in Dart SDK template and test files. - Updated test cases across various platforms to incorporate Channel responses. - Modified web and node.js files to import Channel functionality. --- src/SDK/Language/Apple.php | 2 +- src/SDK/Language/Dart.php | 5 + templates/apple/Sources/Channel.swift.twig | 108 +++++++++++++++++++++ templates/dart/test/channel_test.dart.twig | 3 +- templates/flutter/lib/package.dart.twig | 1 + tests/Android14Java11Test.php | 7 +- tests/Android14Java17Test.php | 1 + tests/Android14Java8Test.php | 7 +- tests/Android5Java17Test.php | 1 + tests/AppleSwift56Test.php | 1 + tests/Base.php | 23 +++++ tests/FlutterBetaTest.php | 1 + tests/FlutterStableTest.php | 1 + tests/WebChromiumTest.php | 1 + tests/WebNodeTest.php | 1 + tests/languages/android/Tests.kt | 1 + tests/languages/node/test.js | 1 + tests/languages/web/index.html | 2 +- tests/languages/web/node.js | 2 +- 19 files changed, 158 insertions(+), 11 deletions(-) create mode 100644 templates/apple/Sources/Channel.swift.twig diff --git a/src/SDK/Language/Apple.php b/src/SDK/Language/Apple.php index 3f409b5936..08f45a6046 100644 --- a/src/SDK/Language/Apple.php +++ b/src/SDK/Language/Apple.php @@ -73,7 +73,7 @@ public function getFiles(): array [ 'scope' => 'default', 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Channel.swift', - 'template' => 'swift/Sources/Channel.swift.twig', + 'template' => 'apple/Sources/Channel.swift.twig', ], [ 'scope' => 'default', diff --git a/src/SDK/Language/Dart.php b/src/SDK/Language/Dart.php index 01e1975099..be3067cda4 100644 --- a/src/SDK/Language/Dart.php +++ b/src/SDK/Language/Dart.php @@ -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', diff --git a/templates/apple/Sources/Channel.swift.twig b/templates/apple/Sources/Channel.swift.twig new file mode 100644 index 0000000000..ba14f2f20b --- /dev/null +++ b/templates/apple/Sources/Channel.swift.twig @@ -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 + } + + /** + * 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 + } +} + diff --git a/templates/dart/test/channel_test.dart.twig b/templates/dart/test/channel_test.dart.twig index 2a3dd59535..db3cf04fe3 100644 --- a/templates/dart/test/channel_test.dart.twig +++ b/templates/dart/test/channel_test.dart.twig @@ -108,5 +108,4 @@ void main() { 'memberships.membership1.update'); }); }); -} - +} \ No newline at end of file diff --git a/templates/flutter/lib/package.dart.twig b/templates/flutter/lib/package.dart.twig index 51965ccb99..515452fa64 100644 --- a/templates/flutter/lib/package.dart.twig +++ b/templates/flutter/lib/package.dart.twig @@ -30,6 +30,7 @@ part 'query.dart'; part 'permission.dart'; part 'role.dart'; part 'id.dart'; +part 'channel.dart'; part 'operator.dart'; {% for service in spec.services %} part 'services/{{service.name | caseSnake}}.dart'; diff --git a/tests/Android14Java11Test.php b/tests/Android14Java11Test.php index b6e114f404..1bc2f2360e 100644 --- a/tests/Android14Java11Test.php +++ b/tests/Android14Java11Test.php @@ -24,15 +24,16 @@ class Android14Java11Test extends Base ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, ...Base::GENERAL_RESPONSES, - ...Base::LARGE_FILE_RESPONSES, - ...Base::LARGE_FILE_RESPONSES, - ...Base::LARGE_FILE_RESPONSES, + // ...Base::LARGE_FILE_RESPONSES, + // ...Base::LARGE_FILE_RESPONSES, + // ...Base::LARGE_FILE_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::REALTIME_RESPONSES, // ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, + ...Base::CHANNEL_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Android14Java17Test.php b/tests/Android14Java17Test.php index aede9c7af1..e6e1b7c458 100644 --- a/tests/Android14Java17Test.php +++ b/tests/Android14Java17Test.php @@ -32,6 +32,7 @@ class Android14Java17Test extends Base ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, + ...Base::CHANNEL_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Android14Java8Test.php b/tests/Android14Java8Test.php index 0355e77b04..84a81ebe4a 100644 --- a/tests/Android14Java8Test.php +++ b/tests/Android14Java8Test.php @@ -24,15 +24,16 @@ class Android14Java8Test extends Base ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, ...Base::GENERAL_RESPONSES, - ...Base::LARGE_FILE_RESPONSES, - ...Base::LARGE_FILE_RESPONSES, - ...Base::LARGE_FILE_RESPONSES, + // ...Base::LARGE_FILE_RESPONSES, + // ...Base::LARGE_FILE_RESPONSES, + // ...Base::LARGE_FILE_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::REALTIME_RESPONSES, // ...Base::COOKIE_RESPONSES, ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, + ...Base::CHANNEL_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Android5Java17Test.php b/tests/Android5Java17Test.php index 9677e0f00d..1a58bb3271 100644 --- a/tests/Android5Java17Test.php +++ b/tests/Android5Java17Test.php @@ -32,6 +32,7 @@ class Android5Java17Test extends Base ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, + ...Base::CHANNEL_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/AppleSwift56Test.php b/tests/AppleSwift56Test.php index b4a6709f14..36852cae07 100644 --- a/tests/AppleSwift56Test.php +++ b/tests/AppleSwift56Test.php @@ -31,6 +31,7 @@ class AppleSwift56Test extends Base ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, + ...Base::CHANNEL_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/Base.php b/tests/Base.php index 4917f2d9ad..2a2b3e17e8 100644 --- a/tests/Base.php +++ b/tests/Base.php @@ -163,6 +163,29 @@ abstract class Base extends TestCase 'custom_id' ]; + protected const CHANNEL_HELPER_RESPONSES = [ + 'databases.*.collections.*.documents.*', + 'databases.db1.collections.col1.documents.doc1', + 'databases.db1.collections.col1.documents.doc1.create', + 'tablesdb.*.tables.*.rows.*', + 'tablesdb.db1.tables.table1.rows.row1', + 'tablesdb.db1.tables.table1.rows.row1.update', + 'account.*', + 'account.user123', + 'buckets.*.files.*', + 'buckets.bucket1.files.file1', + 'buckets.bucket1.files.file1.delete', + 'functions.*.executions.*', + 'functions.func1.executions.exec1', + 'functions.func1.executions.exec1.create', + 'teams.*', + 'teams.team1', + 'teams.team1.create', + 'memberships.*', + 'memberships.membership1', + 'memberships.membership1.update', + ]; + protected const OPERATOR_HELPER_RESPONSES = [ '{"method":"increment","values":[1]}', '{"method":"increment","values":[5,100]}', diff --git a/tests/FlutterBetaTest.php b/tests/FlutterBetaTest.php index 366f3d1320..32b99cf557 100644 --- a/tests/FlutterBetaTest.php +++ b/tests/FlutterBetaTest.php @@ -31,6 +31,7 @@ class FlutterBetaTest extends Base ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, + ...Base::CHANNEL_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/FlutterStableTest.php b/tests/FlutterStableTest.php index 62521f7800..1c98f9ae3b 100644 --- a/tests/FlutterStableTest.php +++ b/tests/FlutterStableTest.php @@ -31,6 +31,7 @@ class FlutterStableTest extends Base ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, + ...Base::CHANNEL_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/WebChromiumTest.php b/tests/WebChromiumTest.php index 8e609e383e..9a85edb8d4 100644 --- a/tests/WebChromiumTest.php +++ b/tests/WebChromiumTest.php @@ -35,6 +35,7 @@ class WebChromiumTest extends Base ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, + ...Base::CHANNEL_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/WebNodeTest.php b/tests/WebNodeTest.php index dc8e48275e..8e09dcdf6a 100644 --- a/tests/WebNodeTest.php +++ b/tests/WebNodeTest.php @@ -35,6 +35,7 @@ class WebNodeTest extends Base ...Base::QUERY_HELPER_RESPONSES, ...Base::PERMISSION_HELPER_RESPONSES, ...Base::ID_HELPER_RESPONSES, + ...Base::CHANNEL_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES ]; } diff --git a/tests/languages/android/Tests.kt b/tests/languages/android/Tests.kt index eaed5dd53d..579bc00193 100644 --- a/tests/languages/android/Tests.kt +++ b/tests/languages/android/Tests.kt @@ -7,6 +7,7 @@ import io.appwrite.exceptions.AppwriteException import io.appwrite.Permission import io.appwrite.Role import io.appwrite.ID +import io.appwrite.Channel import io.appwrite.Query import io.appwrite.Operator import io.appwrite.Condition diff --git a/tests/languages/node/test.js b/tests/languages/node/test.js index c52aafd28b..e0d43bb6bd 100644 --- a/tests/languages/node/test.js +++ b/tests/languages/node/test.js @@ -4,6 +4,7 @@ const { Query, Role, ID, + Channel, Operator, Condition, MockType, diff --git a/tests/languages/web/index.html b/tests/languages/web/index.html index 8aced2b342..f85cbad048 100644 --- a/tests/languages/web/index.html +++ b/tests/languages/web/index.html @@ -21,7 +21,7 @@ let responseRealtime = 'Realtime failed!'; // Init SDK - const { Client, Foo, Bar, General, Realtime, Query, Permission, Role, ID, Operator, Condition, MockType } = Appwrite; + const { Client, Foo, Bar, General, Realtime, Query, Permission, Role, ID, Channel, Operator, Condition, MockType } = Appwrite; const client = new Client(); const foo = new Foo(client); diff --git a/tests/languages/web/node.js b/tests/languages/web/node.js index 8e82b34781..45bf89fee4 100644 --- a/tests/languages/web/node.js +++ b/tests/languages/web/node.js @@ -1,4 +1,4 @@ -const { Client, Foo, Bar, General, Query, Permission, Role, ID, Operator, Condition, MockType } = require('./dist/cjs/sdk.js'); +const { Client, Foo, Bar, General, Query, Permission, Role, ID, Channel, Operator, Condition, MockType } = require('./dist/cjs/sdk.js'); async function start() { let response; From 5d19169d9914fb7d415622218654ad9096527d2a Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 18 Dec 2025 23:24:58 +0530 Subject: [PATCH 03/26] reverted changes --- tests/Android14Java11Test.php | 6 +++--- tests/Android14Java8Test.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Android14Java11Test.php b/tests/Android14Java11Test.php index 1bc2f2360e..b01e4ec93f 100644 --- a/tests/Android14Java11Test.php +++ b/tests/Android14Java11Test.php @@ -24,9 +24,9 @@ class Android14Java11Test extends Base ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, ...Base::GENERAL_RESPONSES, - // ...Base::LARGE_FILE_RESPONSES, - // ...Base::LARGE_FILE_RESPONSES, - // ...Base::LARGE_FILE_RESPONSES, + ...Base::LARGE_FILE_RESPONSES, + ...Base::LARGE_FILE_RESPONSES, + ...Base::LARGE_FILE_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::REALTIME_RESPONSES, // ...Base::COOKIE_RESPONSES, diff --git a/tests/Android14Java8Test.php b/tests/Android14Java8Test.php index 84a81ebe4a..4bb086e252 100644 --- a/tests/Android14Java8Test.php +++ b/tests/Android14Java8Test.php @@ -24,9 +24,9 @@ class Android14Java8Test extends Base ...Base::FOO_RESPONSES, ...Base::BAR_RESPONSES, ...Base::GENERAL_RESPONSES, - // ...Base::LARGE_FILE_RESPONSES, - // ...Base::LARGE_FILE_RESPONSES, - // ...Base::LARGE_FILE_RESPONSES, + ...Base::LARGE_FILE_RESPONSES, + ...Base::LARGE_FILE_RESPONSES, + ...Base::LARGE_FILE_RESPONSES, ...Base::EXCEPTION_RESPONSES, ...Base::REALTIME_RESPONSES, // ...Base::COOKIE_RESPONSES, From b18671f078e7f64aa18050fea94b66d13e093806 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Thu, 18 Dec 2025 23:54:20 +0530 Subject: [PATCH 04/26] Refactor Channel references to use NIOCore.Channel and remove unused Channel helper tests from node and swift --- templates/apple/Sources/Channel.swift.twig | 2 + templates/swift/Sources/Channel.swift.twig | 108 ------------------ .../WebSockets/WebSocketClient.swift.twig | 20 ++-- .../WebSocketClientDelegate.swift.twig | 8 +- tests/languages/node/test.js | 23 ---- tests/languages/swift/Tests.swift | 22 ---- 6 files changed, 16 insertions(+), 167 deletions(-) delete mode 100644 templates/swift/Sources/Channel.swift.twig diff --git a/templates/apple/Sources/Channel.swift.twig b/templates/apple/Sources/Channel.swift.twig index ba14f2f20b..aec5ada8fd 100644 --- a/templates/apple/Sources/Channel.swift.twig +++ b/templates/apple/Sources/Channel.swift.twig @@ -1,3 +1,5 @@ +import Foundation + public class Channel { /** * Generate a database channel string. diff --git a/templates/swift/Sources/Channel.swift.twig b/templates/swift/Sources/Channel.swift.twig deleted file mode 100644 index ba14f2f20b..0000000000 --- a/templates/swift/Sources/Channel.swift.twig +++ /dev/null @@ -1,108 +0,0 @@ -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 - } - - /** - * 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 - } -} - diff --git a/templates/swift/Sources/WebSockets/WebSocketClient.swift.twig b/templates/swift/Sources/WebSockets/WebSocketClient.swift.twig index 0e6c3e7ee6..7c004d295c 100644 --- a/templates/swift/Sources/WebSockets/WebSocketClient.swift.twig +++ b/templates/swift/Sources/WebSockets/WebSocketClient.swift.twig @@ -32,7 +32,7 @@ public class WebSocketClient { private let channelQueue = DispatchQueue(label: WEBSOCKET_CHANNEL_QUEUE) private let threadGroupQueue = DispatchQueue(label: WEBSOCKET_THREAD_QUEUE) - var channel: Channel? { + var channel: NIOCore.Channel? { get { return channelQueue.sync { _channel } } @@ -40,7 +40,7 @@ public class WebSocketClient { channelQueue.sync { _channel = newValue } } } - private var _channel: Channel? = nil + private var _channel: NIOCore.Channel? = nil var threadGroup: MultiThreadedEventLoopGroup? { get { @@ -61,8 +61,8 @@ public class WebSocketClient { // MARK: - Stored callbacks - private var _openCallback: (Channel) -> Void = { _ in } - var onOpen: (Channel) -> Void { + private var _openCallback: (NIOCore.Channel) -> Void = { _ in } + var onOpen: (NIOCore.Channel) -> Void { get { return locker.sync { return _openCallback @@ -75,8 +75,8 @@ public class WebSocketClient { } } - private var _closeCallback: (Channel, Data) -> Void = { _,_ in } - var onClose: (Channel, Data) -> Void { + private var _closeCallback: (NIOCore.Channel, Data) -> Void = { _,_ in } + var onClose: (NIOCore.Channel, Data) -> Void { get { return locker.sync { return _closeCallback @@ -137,7 +137,7 @@ public class WebSocketClient { /// /// - parameters: /// - callback: Callback to fie when a WebSocket connection is opened - public func onOpen(_ callback: @escaping (Channel) -> Void) { + public func onOpen(_ callback: @escaping (NIOCore.Channel) -> Void) { onOpen = callback } @@ -161,7 +161,7 @@ public class WebSocketClient { /// /// - parameters: /// - callback: Callback to fie when a WebSocket close message is received - public func onClose(_ callback: @escaping (Channel, Data) -> Void) { + public func onClose(_ callback: @escaping (NIOCore.Channel, Data) -> Void) { onClose = callback } @@ -262,7 +262,7 @@ public class WebSocketClient { .get() } - private func openChannel(channel: Channel) -> EventLoopFuture { + private func openChannel(channel: NIOCore.Channel) -> EventLoopFuture { let httpHandler = HTTPHandler(client: self, headers: headers) let basicUpgrader = NIOWebSocketClientUpgrader( @@ -290,7 +290,7 @@ public class WebSocketClient { } } - private func upgradePipelineHandler(channel: Channel, response: HTTPResponseHead) -> EventLoopFuture { + private func upgradePipelineHandler(channel: NIOCore.Channel, response: HTTPResponseHead) -> EventLoopFuture { let handler = MessageHandler(client: self) if response.status == .switchingProtocols { diff --git a/templates/swift/Sources/WebSockets/WebSocketClientDelegate.swift.twig b/templates/swift/Sources/WebSockets/WebSocketClientDelegate.swift.twig index d1556acb99..d6327e0d97 100644 --- a/templates/swift/Sources/WebSockets/WebSocketClientDelegate.swift.twig +++ b/templates/swift/Sources/WebSockets/WebSocketClientDelegate.swift.twig @@ -4,22 +4,22 @@ import NIOHTTP1 /// Handles messages received by a connected WebSocket server. public protocol WebSocketClientDelegate : AnyObject { - func onOpen(channel: Channel) + func onOpen(channel: NIOCore.Channel) func onMessage(text: String) throws func onMessage(data: Data) throws - func onClose(channel: Channel, data: Data) + func onClose(channel: NIOCore.Channel, data: Data) func onError(error: Swift.Error?, status: HTTPResponseStatus?) throws } // Add empty default implementations extension WebSocketClientDelegate { - public func onOpen(channel: Channel) { + public func onOpen(channel: NIOCore.Channel) { } public func onMessage(text: String) { } public func onMessage(data: Data) { } - public func onClose(channel: Channel, data: Data) { + public func onClose(channel: NIOCore.Channel, data: Data) { } public func onError(error: Swift.Error?, status: HTTPResponseStatus?) throws { } diff --git a/tests/languages/node/test.js b/tests/languages/node/test.js index e0d43bb6bd..ee0fd703f5 100644 --- a/tests/languages/node/test.js +++ b/tests/languages/node/test.js @@ -4,7 +4,6 @@ const { Query, Role, ID, - Channel, Operator, Condition, MockType, @@ -327,28 +326,6 @@ async function start() { console.log(ID.unique()); console.log(ID.custom('custom_id')); - // Channel helper tests - console.log(Channel.database()); - console.log(Channel.database('db1', 'col1', 'doc1')); - console.log(Channel.database('db1', 'col1', 'doc1', 'create')); - console.log(Channel.tablesdb()); - console.log(Channel.tablesdb('db1', 'table1', 'row1')); - console.log(Channel.tablesdb('db1', 'table1', 'row1', 'update')); - console.log(Channel.account()); - console.log(Channel.account('user123')); - console.log(Channel.files()); - console.log(Channel.files('bucket1', 'file1')); - console.log(Channel.files('bucket1', 'file1', 'delete')); - console.log(Channel.executions()); - console.log(Channel.executions('func1', 'exec1')); - console.log(Channel.executions('func1', 'exec1', 'create')); - console.log(Channel.teams()); - console.log(Channel.teams('team1')); - console.log(Channel.teams('team1', 'create')); - console.log(Channel.memberships()); - console.log(Channel.memberships('membership1')); - console.log(Channel.memberships('membership1', 'update')); - // Operator helper tests console.log(Operator.increment(1)); console.log(Operator.increment(5, 100)); diff --git a/tests/languages/swift/Tests.swift b/tests/languages/swift/Tests.swift index d682ff60de..8719dc6466 100644 --- a/tests/languages/swift/Tests.swift +++ b/tests/languages/swift/Tests.swift @@ -235,28 +235,6 @@ class Tests: XCTestCase { print(ID.unique()) print(ID.custom("custom_id")) - // Channel helper tests - print(Channel.database()) - print(Channel.database(databaseId: "db1", collectionId: "col1", documentId: "doc1")) - print(Channel.database(databaseId: "db1", collectionId: "col1", documentId: "doc1", action: "create")) - print(Channel.tablesdb()) - print(Channel.tablesdb(databaseId: "db1", tableId: "table1", rowId: "row1")) - print(Channel.tablesdb(databaseId: "db1", tableId: "table1", rowId: "row1", action: "update")) - print(Channel.account()) - print(Channel.account(userId: "user123")) - print(Channel.files()) - print(Channel.files(bucketId: "bucket1", fileId: "file1")) - print(Channel.files(bucketId: "bucket1", fileId: "file1", action: "delete")) - print(Channel.executions()) - print(Channel.executions(functionId: "func1", executionId: "exec1")) - print(Channel.executions(functionId: "func1", executionId: "exec1", action: "create")) - print(Channel.teams()) - print(Channel.teams(teamId: "team1")) - print(Channel.teams(teamId: "team1", action: "create")) - print(Channel.memberships()) - print(Channel.memberships(membershipId: "membership1")) - print(Channel.memberships(membershipId: "membership1", action: "update")) - // Operator helper tests print(Operator.increment(1)) print(Operator.increment(5, max: 100)) From fd57b6560b7589d8ed13dab823ff9fe4fca365d5 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 19 Dec 2025 18:24:49 +0530 Subject: [PATCH 05/26] Refractor channel to use builder pattern --- src/SDK/Language/Flutter.php | 2 +- .../src/main/java/io/package/Channel.kt.twig | 218 ++++++++----- .../java/io/package/services/Realtime.kt.twig | 31 ++ templates/apple/Sources/Channel.swift.twig | 289 +++++++++++++---- .../Sources/Services/Realtime.swift.twig | 56 +++- templates/dart/lib/channel.dart.twig | 116 ------- templates/flutter/lib/channel.dart.twig | 239 ++++++++++++++ templates/flutter/lib/src/realtime.dart.twig | 10 +- .../flutter/lib/src/realtime_base.dart.twig | 2 +- .../lib/src/realtime_browser.dart.twig | 2 +- .../flutter/lib/src/realtime_io.dart.twig | 2 +- .../flutter/lib/src/realtime_mixin.dart.twig | 17 +- templates/web/src/channel.ts.twig | 295 +++++++++++++----- templates/web/src/client.ts.twig | 33 +- templates/web/src/services/realtime.ts.twig | 47 ++- tests/languages/android/Tests.kt | 36 +-- tests/languages/apple/Tests.swift | 38 +-- tests/languages/flutter/tests.dart | 38 +-- tests/languages/web/index.html | 36 +-- tests/languages/web/node.js | 36 +-- 20 files changed, 1091 insertions(+), 452 deletions(-) delete mode 100644 templates/dart/lib/channel.dart.twig create mode 100644 templates/flutter/lib/channel.dart.twig diff --git a/src/SDK/Language/Flutter.php b/src/SDK/Language/Flutter.php index 613720c094..0643939a38 100644 --- a/src/SDK/Language/Flutter.php +++ b/src/SDK/Language/Flutter.php @@ -83,7 +83,7 @@ public function getFiles(): array [ 'scope' => 'default', 'destination' => '/lib/channel.dart', - 'template' => 'dart/lib/channel.dart.twig', + 'template' => 'flutter/lib/channel.dart.twig', ], [ 'scope' => 'default', diff --git a/templates/android/library/src/main/java/io/package/Channel.kt.twig b/templates/android/library/src/main/java/io/package/Channel.kt.twig index 10f9fcd410..4fe27e4324 100644 --- a/templates/android/library/src/main/java/io/package/Channel.kt.twig +++ b/templates/android/library/src/main/java/io/package/Channel.kt.twig @@ -1,115 +1,189 @@ package {{ sdk.namespace | caseDot }} -/** - * Helper class to generate channel strings for realtime subscriptions. - */ +class ResolvedChannel(private val value: String) { + override fun toString(): String { + return value + } +} + +abstract class ActionChannel(protected val base: String) { + fun create(): ResolvedChannel { + return ResolvedChannel("$base.create") + } + + fun update(): ResolvedChannel { + return ResolvedChannel("$base.update") + } + + fun delete(): ResolvedChannel { + return ResolvedChannel("$base.delete") + } + + override fun toString(): String { + return base + } +} + +class DocumentChannel(base: String) : ActionChannel(base) + +class CollectionChannel( + private val databaseId: String, + private val collectionId: String +) { + private val base = "databases.$databaseId.collections.$collectionId" + + fun document(documentId: String = "*"): DocumentChannel { + return DocumentChannel("$base.documents.$documentId") + } + + override fun toString(): String { + return base + } +} + +class DatabaseChannel(private val databaseId: String) { + private val base = "databases.$databaseId" + + fun collection(collectionId: String = "*"): CollectionChannel { + return CollectionChannel(databaseId, collectionId) + } + + override fun toString(): String { + return base + } +} + +class RowChannel(base: String) : ActionChannel(base) + +class TableChannel( + private val databaseId: String, + private val tableId: String +) { + private val base = "tablesdb.$databaseId.tables.$tableId" + + fun row(rowId: String = "*"): RowChannel { + return RowChannel("$base.rows.$rowId") + } + + override fun toString(): String { + return base + } +} + +class TablesDBChannel(private val databaseId: String) { + private val base = "tablesdb.$databaseId" + + fun table(tableId: String = "*"): TableChannel { + return TableChannel(databaseId, tableId) + } + + override fun toString(): String { + return base + } +} + +class FileChannel(base: String) : ActionChannel(base) + +class BucketChannel(private val bucketId: String) { + private val base = "buckets.$bucketId" + + fun file(fileId: String = "*"): FileChannel { + return FileChannel("$base.files.$fileId") + } + + override fun toString(): String { + return base + } +} + +class ExecutionChannel(base: String) : ActionChannel(base) + +class FunctionChannel(private val functionId: String) { + private val base = "functions.$functionId" + + fun execution(executionId: String = "*"): ExecutionChannel { + return ExecutionChannel("$base.executions.$executionId") + } + + override fun toString(): String { + return base + } +} + +class TeamChannel(teamId: String = "*") : ActionChannel("teams.$teamId") + +class MembershipChannel(membershipId: String = "*") : ActionChannel("memberships.$membershipId") + class Channel { companion object { - /** - * Generate a database channel string. + * Generate a database channel builder. * * @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 + * @returns DatabaseChannel */ - 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 + fun database(databaseId: String = "*"): DatabaseChannel { + return DatabaseChannel(databaseId) } /** - * Generate a tables database channel string. + * Generate a tables database channel builder. * * @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 + * @returns TablesDBChannel */ - 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 + fun tablesdb(databaseId: String = "*"): TablesDBChannel { + return TablesDBChannel(databaseId) } /** - * Generate an account channel string. + * Generate a buckets channel builder. * - * @param userId The user ID (default: "*") - * @returns The channel string + * @param bucketId The bucket ID (default: "*") + * @returns BucketChannel */ - fun account(userId: String = "*"): String { - return "account.$userId" + fun buckets(bucketId: String = "*"): BucketChannel { + return BucketChannel(bucketId) } /** - * Generate a files channel string. + * Generate a functions channel builder. * - * @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 + * @param functionId The function ID (default: "*") + * @returns FunctionChannel */ - fun files(bucketId: String = "*", fileId: String = "*", action: String? = null): String { - var channel = "buckets.$bucketId.files.$fileId" - if (action != null) { - channel += ".$action" - } - return channel + fun functions(functionId: String = "*"): FunctionChannel { + return FunctionChannel(functionId) } /** - * Generate an executions channel string. + * Generate a teams channel builder. * - * @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 + * @param teamId The team ID (default: "*") + * @returns TeamChannel */ - fun executions(functionId: String = "*", executionId: String = "*", action: String? = null): String { - var channel = "functions.$functionId.executions.$executionId" - if (action != null) { - channel += ".$action" - } - return channel + fun teams(teamId: String = "*"): TeamChannel { + return TeamChannel(teamId) } /** - * Generate a teams channel string. + * Generate a memberships channel builder. * - * @param teamId The team ID (default: "*") - * @param action Optional action: "create", "update", or "delete" (default: null) - * @returns The channel string + * @param membershipId The membership ID (default: "*") + * @returns MembershipChannel */ - fun teams(teamId: String = "*", action: String? = null): String { - var channel = "teams.$teamId" - if (action != null) { - channel += ".$action" - } - return channel + fun memberships(membershipId: String = "*"): MembershipChannel { + return MembershipChannel(membershipId) } /** - * Generate a memberships channel string. + * Generate an account channel string. * - * @param membershipId The membership ID (default: "*") - * @param action Optional action: "create", "update", or "delete" (default: null) + * @param userId The user ID (default: "*") * @returns The channel string */ - fun memberships(membershipId: String = "*", action: String? = null): String { - var channel = "memberships.$membershipId" - if (action != null) { - channel += ".$action" - } - return channel + fun account(userId: String = "*"): String { + return "account.$userId" } } } diff --git a/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig b/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig index 26de08d0ee..2e887dfe3a 100644 --- a/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig +++ b/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig @@ -109,6 +109,25 @@ class Realtime(client: Client) : Service(client), CoroutineScope { else -> 60000L } + /** + * Convert channel value to string + */ + private fun channelToString(channel: Any): String { + return when (channel) { + is String -> channel + else -> channel.toString() + } + } + + fun subscribe( + vararg channels: Any, + callback: (RealtimeResponseEvent) -> Unit, + ) = subscribe( + channels = channels.map { channelToString(it) }.toTypedArray(), + Any::class.java, + callback + ) + fun subscribe( vararg channels: String, callback: (RealtimeResponseEvent) -> Unit, @@ -118,6 +137,18 @@ class Realtime(client: Client) : Service(client), CoroutineScope { callback ) + fun subscribe( + vararg channels: Any, + payloadType: Class, + callback: (RealtimeResponseEvent) -> Unit, + ): RealtimeSubscription { + return subscribe( + channels = channels.map { channelToString(it) }.toTypedArray(), + payloadType = payloadType, + callback = callback + ) + } + fun subscribe( vararg channels: String, payloadType: Class, diff --git a/templates/apple/Sources/Channel.swift.twig b/templates/apple/Sources/Channel.swift.twig index aec5ada8fd..599d9d5958 100644 --- a/templates/apple/Sources/Channel.swift.twig +++ b/templates/apple/Sources/Channel.swift.twig @@ -1,110 +1,261 @@ import Foundation +public class ResolvedChannel { + private let value: String + + init(value: String) { + self.value = value + } + + public func toString() -> String { + return value + } +} + +open class ActionChannel { + let base: String + + init(base: String) { + self.base = base + } + + public func create() -> ResolvedChannel { + return ResolvedChannel(value: "\(base).create") + } + + public func update() -> ResolvedChannel { + return ResolvedChannel(value: "\(base).update") + } + + public func delete() -> ResolvedChannel { + return ResolvedChannel(value: "\(base).delete") + } + + public func toString() -> String { + return base + } +} + +/** + * ---------------------------- + * Database → Collection → Document + * ---------------------------- + */ +public class DocumentChannel: ActionChannel {} + +public class CollectionChannel { + private let base: String + private let databaseId: String + private let collectionId: String + + init(databaseId: String, collectionId: String) { + self.databaseId = databaseId + self.collectionId = collectionId + self.base = "databases.\(databaseId).collections.\(collectionId)" + } + + public func document(_ documentId: String = "*") -> DocumentChannel { + return DocumentChannel(base: "\(base).documents.\(documentId)") + } + + public func toString() -> String { + return base + } +} + +public class DatabaseChannel { + private let base: String + private let databaseId: String + + init(databaseId: String) { + self.databaseId = databaseId + self.base = "databases.\(databaseId)" + } + + public func collection(_ collectionId: String = "*") -> CollectionChannel { + return CollectionChannel(databaseId: databaseId, collectionId: collectionId) + } + + public func toString() -> String { + return base + } +} + +/** + * ---------------------------- + * TablesDB → Table → Row + * ---------------------------- + */ +public class RowChannel: ActionChannel {} + +public class TableChannel { + private let base: String + private let databaseId: String + private let tableId: String + + init(databaseId: String, tableId: String) { + self.databaseId = databaseId + self.tableId = tableId + self.base = "tablesdb.\(databaseId).tables.\(tableId)" + } + + public func row(_ rowId: String = "*") -> RowChannel { + return RowChannel(base: "\(base).rows.\(rowId)") + } + + public func toString() -> String { + return base + } +} + +public class TablesDBChannel { + private let base: String + private let databaseId: String + + init(databaseId: String) { + self.databaseId = databaseId + self.base = "tablesdb.\(databaseId)" + } + + public func table(_ tableId: String = "*") -> TableChannel { + return TableChannel(databaseId: databaseId, tableId: tableId) + } + + public func toString() -> String { + return base + } +} + +/** + * ---------------------------- + * Buckets → File + * ---------------------------- + */ +public class FileChannel: ActionChannel {} + +public class BucketChannel { + private let base: String + private let bucketId: String + + init(bucketId: String) { + self.bucketId = bucketId + self.base = "buckets.\(bucketId)" + } + + public func file(_ fileId: String = "*") -> FileChannel { + return FileChannel(base: "\(base).files.\(fileId)") + } + + public func toString() -> String { + return base + } +} + +/** + * ---------------------------- + * Functions → Execution + * ---------------------------- + */ +public class ExecutionChannel: ActionChannel {} + +public class FunctionChannel { + private let base: String + private let functionId: String + + init(functionId: String) { + self.functionId = functionId + self.base = "functions.\(functionId)" + } + + public func execution(_ executionId: String = "*") -> ExecutionChannel { + return ExecutionChannel(base: "\(base).executions.\(executionId)") + } + + public func toString() -> String { + return base + } +} + +public class TeamChannel: ActionChannel { + init(teamId: String = "*") { + super.init(base: "teams.\(teamId)") + } +} + +public class MembershipChannel: ActionChannel { + init(membershipId: String = "*") { + super.init(base: "memberships.\(membershipId)") + } +} + public class Channel { /** - * Generate a database channel string. + * Generate a database channel builder. * * @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 + * @returns DatabaseChannel */ - 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 + public static func database(_ databaseId: String = "*") -> DatabaseChannel { + return DatabaseChannel(databaseId: databaseId) } /** - * Generate a tables database channel string. + * Generate a tables database channel builder. * * @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 + * @returns TablesDBChannel */ - 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 + public static func tablesdb(_ databaseId: String = "*") -> TablesDBChannel { + return TablesDBChannel(databaseId: databaseId) } /** - * Generate an account channel string. + * Generate a buckets channel builder. * - * @param userId The user ID (default: "*") - * @returns The channel string + * @param bucketId The bucket ID (default: "*") + * @returns BucketChannel */ - public static func account(userId: String = "*") -> String { - return "account.\(userId)" + public static func buckets(_ bucketId: String = "*") -> BucketChannel { + return BucketChannel(bucketId: bucketId) } /** - * Generate a files channel string. + * Generate a functions channel builder. * - * @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 + * @param functionId The function ID (default: "*") + * @returns FunctionChannel */ - 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 + public static func functions(_ functionId: String = "*") -> FunctionChannel { + return FunctionChannel(functionId: functionId) } /** - * Generate an executions channel string. + * Generate a teams channel builder. * - * @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 + * @param teamId The team ID (default: "*") + * @returns TeamChannel */ - 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 + public static func teams(_ teamId: String = "*") -> TeamChannel { + return TeamChannel(teamId: teamId) } /** - * Generate a teams channel string. + * Generate a memberships channel builder. * - * @param teamId The team ID (default: "*") - * @param action Optional action: "create", "update", or "delete" (default: nil) - * @returns The channel string + * @param membershipId The membership ID (default: "*") + * @returns MembershipChannel */ - public static func teams(teamId: String = "*", action: String? = nil) -> String { - var channel = "teams.\(teamId)" - if let action = action { - channel += ".\(action)" - } - return channel + public static func memberships(_ membershipId: String = "*") -> MembershipChannel { + return MembershipChannel(membershipId: membershipId) } /** - * Generate a memberships channel string. + * Generate an account channel string. * - * @param membershipId The membership ID (default: "*") - * @param action Optional action: "create", "update", or "delete" (default: nil) + * @param userId The user ID (default: "*") * @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 + public static func account(_ userId: String = "*") -> String { + return "account.\(userId)" } } - diff --git a/templates/apple/Sources/Services/Realtime.swift.twig b/templates/apple/Sources/Services/Realtime.swift.twig index 5c2c2c401b..a66f009595 100644 --- a/templates/apple/Sources/Services/Realtime.swift.twig +++ b/templates/apple/Sources/Services/Realtime.swift.twig @@ -3,6 +3,33 @@ import AsyncHTTPClient import NIO import NIOHTTP1 +/** + * Protocol for channel values that can be converted to strings + */ +public protocol ChannelValue { + func toString() -> String +} + +extension String: ChannelValue { + public func toString() -> String { + return self + } +} + +extension ResolvedChannel: ChannelValue {} +extension DatabaseChannel: ChannelValue {} +extension CollectionChannel: ChannelValue {} +extension DocumentChannel: ChannelValue {} +extension TablesDBChannel: ChannelValue {} +extension TableChannel: ChannelValue {} +extension RowChannel: ChannelValue {} +extension BucketChannel: ChannelValue {} +extension FileChannel: ChannelValue {} +extension FunctionChannel: ChannelValue {} +extension ExecutionChannel: ChannelValue {} +extension TeamChannel: ChannelValue {} +extension MembershipChannel: ChannelValue {} + open class Realtime : Service { private let TYPE_ERROR = "error" @@ -117,8 +144,15 @@ open class Realtime : Service { } } + /** + * Convert channel value to string + */ + private func channelToString(_ channel: ChannelValue) -> String { + return channel.toString() + } + public func subscribe( - channel: String, + channel: ChannelValue, callback: @escaping (RealtimeResponseEvent) -> Void ) async throws -> RealtimeSubscription { return try await subscribe( @@ -129,23 +163,35 @@ open class Realtime : Service { } public func subscribe( - channels: Set, + channels: [ChannelValue], callback: @escaping (RealtimeResponseEvent) -> Void ) async throws -> RealtimeSubscription { return try await subscribe( - channels: channels, + channels: Set(channels.map { channelToString($0) }), payloadType: String.self, callback: callback ) } public func subscribe( - channel: String, + channel: ChannelValue, payloadType: T.Type, callback: @escaping (RealtimeResponseEvent) -> Void ) async throws -> RealtimeSubscription { return try await subscribe( - channels: [channel], + channels: Set([channelToString(channel)]), + payloadType: T.self, + callback: callback + ) + } + + public func subscribe( + channels: [ChannelValue], + payloadType: T.Type, + callback: @escaping (RealtimeResponseEvent) -> Void + ) async throws -> RealtimeSubscription { + return try await subscribe( + channels: Set(channels.map { channelToString($0) }), payloadType: T.self, callback: callback ) diff --git a/templates/dart/lib/channel.dart.twig b/templates/dart/lib/channel.dart.twig deleted file mode 100644 index 7966dfacb3..0000000000 --- a/templates/dart/lib/channel.dart.twig +++ /dev/null @@ -1,116 +0,0 @@ -part of '{{ language.params.packageName }}.dart'; - -/// Helper class to generate channel strings for realtime subscriptions. -class Channel { - Channel._(); - - /// Generate a database channel string. - /// - /// [databaseId] The database ID (default: '*') - /// [collectionId] The collection ID (default: '*') - /// [documentId] The document ID (default: '*') - /// [action] Optional action: 'create', 'update', or 'delete' (default: null) - static String database({ - String databaseId = '*', - String collectionId = '*', - String documentId = '*', - String? action, - }) { - String channel = 'databases.$databaseId.collections.$collectionId.documents.$documentId'; - if (action != null) { - channel += '.$action'; - } - return channel; - } - - /// Generate a tables database channel string. - /// - /// [databaseId] The database ID (default: '*') - /// [tableId] The table ID (default: '*') - /// [rowId] The row ID (default: '*') - /// [action] Optional action: 'create', 'update', or 'delete' (default: null) - static String tablesdb({ - String databaseId = '*', - String tableId = '*', - String rowId = '*', - String? action, - }) { - String channel = 'tablesdb.$databaseId.tables.$tableId.rows.$rowId'; - if (action != null) { - channel += '.$action'; - } - return channel; - } - - /// Generate an account channel string. - /// - /// [userId] The user ID (default: '*') - static String account({String userId = '*'}) { - return 'account.$userId'; - } - - /// Generate a files channel string. - /// - /// [bucketId] The bucket ID (default: '*') - /// [fileId] The file ID (default: '*') - /// [action] Optional action: 'create', 'update', or 'delete' (default: null) - static String files({ - String bucketId = '*', - String fileId = '*', - String? action, - }) { - String channel = 'buckets.$bucketId.files.$fileId'; - if (action != null) { - channel += '.$action'; - } - return channel; - } - - /// Generate an executions channel string. - /// - /// [functionId] The function ID (default: '*') - /// [executionId] The execution ID (default: '*') - /// [action] Optional action: 'create', 'update', or 'delete' (default: null) - static String executions({ - String functionId = '*', - String executionId = '*', - String? action, - }) { - String channel = 'functions.$functionId.executions.$executionId'; - if (action != null) { - channel += '.$action'; - } - return channel; - } - - /// Generate a teams channel string. - /// - /// [teamId] The team ID (default: '*') - /// [action] Optional action: 'create', 'update', or 'delete' (default: null) - static String teams({ - String teamId = '*', - String? action, - }) { - String channel = 'teams.$teamId'; - if (action != null) { - channel += '.$action'; - } - return channel; - } - - /// Generate a memberships channel string. - /// - /// [membershipId] The membership ID (default: '*') - /// [action] Optional action: 'create', 'update', or 'delete' (default: null) - static String memberships({ - String membershipId = '*', - String? action, - }) { - String channel = 'memberships.$membershipId'; - if (action != null) { - channel += '.$action'; - } - return channel; - } -} - diff --git a/templates/flutter/lib/channel.dart.twig b/templates/flutter/lib/channel.dart.twig new file mode 100644 index 0000000000..1515f01f30 --- /dev/null +++ b/templates/flutter/lib/channel.dart.twig @@ -0,0 +1,239 @@ +part of '{{ language.params.packageName }}.dart'; + +class ResolvedChannel { + final String _value; + + ResolvedChannel(this._value); + + String toString() { + return _value; + } +} + +abstract class ActionChannel { + final String base; + + ActionChannel(this.base); + + ResolvedChannel create() { + return ResolvedChannel('$base.create'); + } + + ResolvedChannel update() { + return ResolvedChannel('$base.update'); + } + + ResolvedChannel delete() { + return ResolvedChannel('$base.delete'); + } + + @override + String toString() { + return base; + } +} + +/** + * ---------------------------- + * Database → Collection → Document + * ---------------------------- + */ +class DocumentChannel extends ActionChannel { + DocumentChannel(String base) : super(base); +} + +class CollectionChannel { + final String base; + final String databaseId; + final String collectionId; + + CollectionChannel(this.databaseId, this.collectionId) + : base = 'databases.$databaseId.collections.$collectionId'; + + DocumentChannel document([String documentId = '*']) { + return DocumentChannel('$base.documents.$documentId'); + } + + @override + String toString() { + return base; + } +} + +class DatabaseChannel { + final String base; + final String databaseId; + + DatabaseChannel(this.databaseId) : base = 'databases.$databaseId'; + + CollectionChannel collection([String collectionId = '*']) { + return CollectionChannel(databaseId, collectionId); + } + + @override + String toString() { + return base; + } +} + +/** + * ---------------------------- + * TablesDB → Table → Row + * ---------------------------- + */ +class RowChannel extends ActionChannel { + RowChannel(String base) : super(base); +} + +class TableChannel { + final String base; + final String databaseId; + final String tableId; + + TableChannel(this.databaseId, this.tableId) + : base = 'tablesdb.$databaseId.tables.$tableId'; + + RowChannel row([String rowId = '*']) { + return RowChannel('$base.rows.$rowId'); + } + + @override + String toString() { + return base; + } +} + +class TablesDBChannel { + final String base; + final String databaseId; + + TablesDBChannel(this.databaseId) : base = 'tablesdb.$databaseId'; + + TableChannel table([String tableId = '*']) { + return TableChannel(databaseId, tableId); + } + + @override + String toString() { + return base; + } +} + +/** + * ---------------------------- + * Buckets → File + * ---------------------------- + */ +class FileChannel extends ActionChannel { + FileChannel(String base) : super(base); +} + +class BucketChannel { + final String base; + final String bucketId; + + BucketChannel(this.bucketId) : base = 'buckets.$bucketId'; + + FileChannel file([String fileId = '*']) { + return FileChannel('$base.files.$fileId'); + } + + @override + String toString() { + return base; + } +} + +/** + * ---------------------------- + * Functions → Execution + * ---------------------------- + */ +class ExecutionChannel extends ActionChannel { + ExecutionChannel(String base) : super(base); +} + +class FunctionChannel { + final String base; + final String functionId; + + FunctionChannel(this.functionId) : base = 'functions.$functionId'; + + ExecutionChannel execution([String executionId = '*']) { + return ExecutionChannel('$base.executions.$executionId'); + } + + @override + String toString() { + return base; + } +} + +class TeamChannel extends ActionChannel { + TeamChannel([String teamId = '*']) : super('teams.$teamId'); +} + +class MembershipChannel extends ActionChannel { + MembershipChannel([String membershipId = '*']) + : super('memberships.$membershipId'); +} + +typedef ChannelValue = Object; + +class Channel { + /// Generate a database channel builder. + /// + /// [databaseId] The database ID (default: "*") + /// Returns [DatabaseChannel] + static DatabaseChannel database([String databaseId = '*']) { + return DatabaseChannel(databaseId); + } + + /// Generate a tables database channel builder. + /// + /// [databaseId] The database ID (default: "*") + /// Returns [TablesDBChannel] + static TablesDBChannel tablesdb([String databaseId = '*']) { + return TablesDBChannel(databaseId); + } + + /// Generate a buckets channel builder. + /// + /// [bucketId] The bucket ID (default: "*") + /// Returns [BucketChannel] + static BucketChannel buckets([String bucketId = '*']) { + return BucketChannel(bucketId); + } + + /// Generate a functions channel builder. + /// + /// [functionId] The function ID (default: "*") + /// Returns [FunctionChannel] + static FunctionChannel functions([String functionId = '*']) { + return FunctionChannel(functionId); + } + + /// Generate a teams channel builder. + /// + /// [teamId] The team ID (default: "*") + /// Returns [TeamChannel] + static TeamChannel teams([String teamId = '*']) { + return TeamChannel(teamId); + } + + /// Generate a memberships channel builder. + /// + /// [membershipId] The membership ID (default: "*") + /// Returns [MembershipChannel] + static MembershipChannel memberships([String membershipId = '*']) { + return MembershipChannel(membershipId); + } + + /// Generate an account channel string. + /// + /// [userId] The user ID (default: "*") + /// Returns The channel string + static String account([String userId = '*']) { + return 'account.$userId'; + } +} \ No newline at end of file diff --git a/templates/flutter/lib/src/realtime.dart.twig b/templates/flutter/lib/src/realtime.dart.twig index e02d89a563..5cf072a168 100644 --- a/templates/flutter/lib/src/realtime.dart.twig +++ b/templates/flutter/lib/src/realtime.dart.twig @@ -42,7 +42,15 @@ abstract class Realtime extends Service { /// subscription.close(); /// ``` /// - RealtimeSubscription subscribe(List channels); + /// You can also use Channel builders: + /// ```dart + /// final subscription = realtime.subscribe([ + /// Channel.database('db').collection('col').document('doc').create(), + /// Channel.buckets('bucket').file('file').update(), + /// 'account.*' + /// ]); + /// ``` + RealtimeSubscription subscribe(List channels); /// The [close code](https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5) set when the WebSocket connection is closed. /// diff --git a/templates/flutter/lib/src/realtime_base.dart.twig b/templates/flutter/lib/src/realtime_base.dart.twig index f9fe5e44bc..4793110742 100644 --- a/templates/flutter/lib/src/realtime_base.dart.twig +++ b/templates/flutter/lib/src/realtime_base.dart.twig @@ -3,5 +3,5 @@ import 'realtime.dart'; abstract class RealtimeBase implements Realtime { @override - RealtimeSubscription subscribe(List channels); + RealtimeSubscription subscribe(List channels); } diff --git a/templates/flutter/lib/src/realtime_browser.dart.twig b/templates/flutter/lib/src/realtime_browser.dart.twig index aa6a3ad149..5d08890664 100644 --- a/templates/flutter/lib/src/realtime_browser.dart.twig +++ b/templates/flutter/lib/src/realtime_browser.dart.twig @@ -35,7 +35,7 @@ class RealtimeBrowser extends RealtimeBase with RealtimeMixin { } @override - RealtimeSubscription subscribe(List channels) { + RealtimeSubscription subscribe(List channels) { return subscribeTo(channels); } } diff --git a/templates/flutter/lib/src/realtime_io.dart.twig b/templates/flutter/lib/src/realtime_io.dart.twig index 27539b251c..dd881725b0 100644 --- a/templates/flutter/lib/src/realtime_io.dart.twig +++ b/templates/flutter/lib/src/realtime_io.dart.twig @@ -43,7 +43,7 @@ class RealtimeIO extends RealtimeBase with RealtimeMixin { /// Use this method to subscribe to a channels and listen to /// realtime events on those channels @override - RealtimeSubscription subscribe(List channels) { + RealtimeSubscription subscribe(List channels) { return subscribeTo(channels); } diff --git a/templates/flutter/lib/src/realtime_mixin.dart.twig b/templates/flutter/lib/src/realtime_mixin.dart.twig index 96e30699b0..e34bc1a9c0 100644 --- a/templates/flutter/lib/src/realtime_mixin.dart.twig +++ b/templates/flutter/lib/src/realtime_mixin.dart.twig @@ -168,18 +168,27 @@ mixin RealtimeMixin { ); } - RealtimeSubscription subscribeTo(List channels) { + /// Convert channel value to string + String _channelToString(Object channel) { + if (channel is String) { + return channel; + } + return channel.toString(); + } + + RealtimeSubscription subscribeTo(List channels) { StreamController controller = StreamController.broadcast(); - _channels.addAll(channels); + final channelStrings = channels.map((ch) => _channelToString(ch)).toList().cast(); + _channels.addAll(channelStrings); Future.delayed(Duration.zero, () => _createSocket()); int id = DateTime.now().microsecondsSinceEpoch; RealtimeSubscription subscription = RealtimeSubscription( controller: controller, - channels: channels, + channels: channelStrings, close: () async { _subscriptions.remove(id); controller.close(); - _cleanup(channels); + _cleanup(channelStrings); if (_channels.isNotEmpty) { await Future.delayed(Duration.zero, () => _createSocket()); diff --git a/templates/web/src/channel.ts.twig b/templates/web/src/channel.ts.twig index fe2cada9e6..ac14ebdf87 100644 --- a/templates/web/src/channel.ts.twig +++ b/templates/web/src/channel.ts.twig @@ -1,111 +1,266 @@ + + +export class ResolvedChannel { + private value: string; + + constructor(value: string) { + this.value = value; + } + + toString(): string { + return this.value; + } +} + +abstract class ActionChannel { + protected base: string; + + constructor(base: string) { + this.base = base; + } + + create(): ResolvedChannel { + return new ResolvedChannel(`${this.base}.create`); + } + + update(): ResolvedChannel { + return new ResolvedChannel(`${this.base}.update`); + } + + delete(): ResolvedChannel { + return new ResolvedChannel(`${this.base}.delete`); + } + + toString(): string { + return this.base; + } +} + +/** + * ---------------------------- + * Database → Collection → Document + * ---------------------------- + */ +class DocumentChannel extends ActionChannel {} + +class CollectionChannel { + private base: string; + + constructor( + private databaseId: string, + private collectionId: string + ) { + this.base = `databases.${databaseId}.collections.${collectionId}`; + } + + document(documentId: string = '*'): DocumentChannel { + return new DocumentChannel( + `${this.base}.documents.${documentId}` + ); + } + + toString(): string { + return this.base; + } +} + +class DatabaseChannel { + private base: string; + + constructor(private databaseId: string) { + this.base = `databases.${databaseId}`; + } + + collection(collectionId: string = '*'): CollectionChannel { + return new CollectionChannel(this.databaseId, collectionId); + } + + toString(): string { + return this.base; + } +} + +/** + * ---------------------------- + * TablesDB → Table → Row + * ---------------------------- + */ +class RowChannel extends ActionChannel {} + +class TableChannel { + private base: string; + + constructor( + private databaseId: string, + private tableId: string + ) { + this.base = `tablesdb.${databaseId}.tables.${tableId}`; + } + + row(rowId: string = '*'): RowChannel { + return new RowChannel( + `${this.base}.rows.${rowId}` + ); + } + + toString(): string { + return this.base; + } +} + +class TablesDBChannel { + private base: string; + + constructor(private databaseId: string) { + this.base = `tablesdb.${databaseId}`; + } + + table(tableId: string = '*'): TableChannel { + return new TableChannel(this.databaseId, tableId); + } + + toString(): string { + return this.base; + } +} + +/** + * ---------------------------- + * Buckets → File + * ---------------------------- + */ +class FileChannel extends ActionChannel {} + +class BucketChannel { + private base: string; + + constructor(private bucketId: string) { + this.base = `buckets.${bucketId}`; + } + + file(fileId: string = '*'): FileChannel { + return new FileChannel( + `${this.base}.files.${fileId}` + ); + } + + toString(): string { + return this.base; + } +} + /** - * Helper class to generate channel strings for realtime subscriptions. + * ---------------------------- + * Functions → Execution + * ---------------------------- */ +class ExecutionChannel extends ActionChannel {} + +class FunctionChannel { + private base: string; + + constructor(private functionId: string) { + this.base = `functions.${functionId}`; + } + + execution(executionId: string = '*'): ExecutionChannel { + return new ExecutionChannel( + `${this.base}.executions.${executionId}` + ); + } + + toString(): string { + return this.base; + } +} + +class TeamChannel extends ActionChannel { + constructor(teamId: string = '*') { + super(`teams.${teamId}`); + } +} + +class MembershipChannel extends ActionChannel { + constructor(membershipId: string = '*') { + super(`memberships.${membershipId}`); + } +} + +/** + * ---------------------------- + * Channel Type (for type checking) + * ---------------------------- + */ +export type ChannelValue = string | ResolvedChannel | DatabaseChannel | CollectionChannel | DocumentChannel | TablesDBChannel | TableChannel | RowChannel | BucketChannel | FileChannel | FunctionChannel | ExecutionChannel | TeamChannel | MembershipChannel; + export class Channel { /** - * Generate a database channel string. + * Generate a database channel builder. * * @param {string} databaseId - * @param {string} collectionId - * @param {string} documentId - * @param {'create'|'update'|'delete'|null} action - * @returns {string} + * @returns {DatabaseChannel} */ - static database(databaseId: string = '*', collectionId: string = '*', documentId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { - let channel = `databases.${databaseId}.collections.${collectionId}.documents.${documentId}`; - if (action) { - channel += `.${action}`; - } - return channel; + static database(databaseId: string = '*'): DatabaseChannel { + return new DatabaseChannel(databaseId); } /** - * Generate a tables database channel string. + * Generate a tables database channel builder. * * @param {string} databaseId - * @param {string} tableId - * @param {string} rowId - * @param {'create'|'update'|'delete'|null} action - * @returns {string} + * @returns {TablesDBChannel} */ - static tablesdb(databaseId: string = '*', tableId: string = '*', rowId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { - let channel = `tablesdb.${databaseId}.tables.${tableId}.rows.${rowId}`; - if (action) { - channel += `.${action}`; - } - return channel; + static tablesdb(databaseId: string = '*'): TablesDBChannel { + return new TablesDBChannel(databaseId); } /** - * Generate an account channel string. + * Generate a buckets channel builder. * - * @param {string} userId - * @returns {string} + * @param {string} bucketId + * @returns {BucketChannel} */ - static account(userId: string = '*'): string { - return `account.${userId}`; + static buckets(bucketId: string = '*'): BucketChannel { + return new BucketChannel(bucketId); } /** - * Generate a files channel string. + * Generate a functions channel builder. * - * @param {string} bucketId - * @param {string} fileId - * @param {'create'|'update'|'delete'|null} action - * @returns {string} + * @param {string} functionId + * @returns {FunctionChannel} */ - static files(bucketId: string = '*', fileId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { - let channel = `buckets.${bucketId}.files.${fileId}`; - if (action) { - channel += `.${action}`; - } - return channel; + static functions(functionId: string = '*'): FunctionChannel { + return new FunctionChannel(functionId); } /** - * Generate an executions channel string. + * Generate a teams channel builder. * - * @param {string} functionId - * @param {string} executionId - * @param {'create'|'update'|'delete'|null} action - * @returns {string} + * @param {string} teamId + * @returns {TeamChannel} */ - static executions(functionId: string = '*', executionId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { - let channel = `functions.${functionId}.executions.${executionId}`; - if (action) { - channel += `.${action}`; - } - return channel; + static teams(teamId: string = '*'): TeamChannel { + return new TeamChannel(teamId); } /** - * Generate a teams channel string. + * Generate a memberships channel builder. * - * @param {string} teamId - * @param {'create'|'update'|'delete'|null} action - * @returns {string} + * @param {string} membershipId + * @returns {MembershipChannel} */ - static teams(teamId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { - let channel = `teams.${teamId}`; - if (action) { - channel += `.${action}`; - } - return channel; + static memberships(membershipId: string = '*'): MembershipChannel { + return new MembershipChannel(membershipId); } /** - * Generate a memberships channel string. + * Generate an account channel string. * - * @param {string} membershipId - * @param {'create'|'update'|'delete'|null} action + * @param {string} userId * @returns {string} */ - static memberships(membershipId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { - let channel = `memberships.${membershipId}`; - if (action) { - channel += `.${action}`; - } - return channel; + static account(userId: string = '*'): string { + return `account.${userId}`; } } - diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 358e30bfb2..639507d297 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -1,4 +1,5 @@ import { Models } from './models'; +import { ChannelValue } from './channel'; /** * Payload type representing a key-value pair with string keys and any values. @@ -552,8 +553,8 @@ class Client { * @deprecated Use the Realtime service instead. * @see Realtime * - * @param {string|string[]} channels - * Channel to subscribe - pass a single channel as a string or multiple with an array of strings. + * @param {string|string[]|ChannelValue|ChannelValue[]} channels + * Channel to subscribe - pass a single channel as a string or Channel builder instance, or multiple with an array. * * Possible channels are: * - account @@ -571,16 +572,34 @@ class Client { * - teams.[ID] * - memberships * - memberships.[ID] + * + * You can also use Channel builders: + * - Channel.database('db').collection('col').document('doc').create() + * - Channel.buckets('bucket').file('file').update() + * - Channel.functions('func').execution('exec').delete() + * - Channel.teams('team').create() + * - Channel.memberships('membership').update() * @param {(payload: RealtimeMessage) => void} callback Is called on every realtime update. * @returns {() => void} Unsubscribes from events. */ - subscribe(channels: string | string[], callback: (payload: RealtimeResponseEvent) => void): () => void { - let channelArray = typeof channels === 'string' ? [channels] : channels; - channelArray.forEach(channel => this.realtime.channels.add(channel)); + subscribe(channels: string | string[] | ChannelValue | ChannelValue[], callback: (payload: RealtimeResponseEvent) => void): () => void { + const channelArray = Array.isArray(channels) ? channels : [channels]; + // Convert Channel instances to strings + const channelStrings = channelArray.map(ch => { + if (typeof ch === 'string') { + return ch; + } + // Channel builder instances have toString() method + if (ch && typeof ch.toString === 'function') { + return ch.toString(); + } + return String(ch); + }); + channelStrings.forEach(channel => this.realtime.channels.add(channel)); const counter = this.realtime.subscriptionsCounter++; this.realtime.subscriptions.set(counter, { - channels: channelArray, + channels: channelStrings, callback }); @@ -588,7 +607,7 @@ class Client { return () => { this.realtime.subscriptions.delete(counter); - this.realtime.cleanUp(channelArray); + this.realtime.cleanUp(channelStrings); this.realtime.connect(); } } diff --git a/templates/web/src/services/realtime.ts.twig b/templates/web/src/services/realtime.ts.twig index d30e716ea1..d03e93903a 100644 --- a/templates/web/src/services/realtime.ts.twig +++ b/templates/web/src/services/realtime.ts.twig @@ -1,4 +1,5 @@ import { {{ spec.title | caseUcfirst}}Exception, Client } from '../client'; +import { ChannelValue } from '../channel'; export type RealtimeSubscription = { close: () => Promise; @@ -237,61 +238,83 @@ export class Realtime { return new Promise(resolve => setTimeout(resolve, ms)); } + /** + * Convert a channel value to a string + * + * @private + * @param {string | ChannelValue} channel - Channel value (string or Channel builder instance) + * @returns {string} Channel string representation + */ + private channelToString(channel: string | ChannelValue): string { + if (typeof channel === 'string') { + return channel; + } + // Channel builder instances have toString() method + if (channel && typeof channel.toString === 'function') { + return channel.toString(); + } + return String(channel); + } + /** * Subscribe to a single channel * - * @param {string} channel - Channel name to subscribe to + * @param {string | ChannelValue} channel - Channel name to subscribe to (string or Channel builder instance) * @param {Function} callback - Callback function to handle events * @returns {Promise} Subscription object with close method */ public async subscribe( - channel: string, + channel: ChannelValue, callback: (event: RealtimeResponseEvent) => void ): Promise; /** * Subscribe to multiple channels * - * @param {string[]} channels - Array of channel names to subscribe to + * @param {(string | ChannelValue)[]} channels - Array of channel names to subscribe to (strings or Channel builder instances) * @param {Function} callback - Callback function to handle events * @returns {Promise} Subscription object with close method */ public async subscribe( - channels: string[], + channels: (string | ChannelValue)[], callback: (event: RealtimeResponseEvent) => void ): Promise; /** * Subscribe to a single channel with typed payload * - * @param {string} channel - Channel name to subscribe to + * @param {string | ChannelValue} channel - Channel name to subscribe to (string or Channel builder instance) * @param {Function} callback - Callback function to handle events with typed payload * @returns {Promise} Subscription object with close method */ public async subscribe( - channel: string, + channel: ChannelValue, callback: (event: RealtimeResponseEvent) => void ): Promise; /** * Subscribe to multiple channels with typed payload * - * @param {string[]} channels - Array of channel names to subscribe to + * @param {(string | ChannelValue)[]} channels - Array of channel names to subscribe to (strings or Channel builder instances) * @param {Function} callback - Callback function to handle events with typed payload * @returns {Promise} Subscription object with close method */ public async subscribe( - channels: string[], + channels: (string | ChannelValue)[], callback: (event: RealtimeResponseEvent) => void ): Promise; public async subscribe( - channelsOrChannel: string | string[], + channelsOrChannel: ChannelValue | (string | ChannelValue)[], callback: (event: RealtimeResponseEvent) => void ): Promise { - const channels = Array.isArray(channelsOrChannel) - ? new Set(channelsOrChannel) - : new Set([channelsOrChannel]); + const channelArray = Array.isArray(channelsOrChannel) + ? channelsOrChannel + : [channelsOrChannel]; + + // Convert all channels to strings + const channelStrings = channelArray.map(ch => this.channelToString(ch)); + const channels = new Set(channelStrings); this.subscriptionsCounter++; const count = this.subscriptionsCounter; diff --git a/tests/languages/android/Tests.kt b/tests/languages/android/Tests.kt index 579bc00193..58013f6473 100644 --- a/tests/languages/android/Tests.kt +++ b/tests/languages/android/Tests.kt @@ -271,26 +271,26 @@ class ServiceTest { writeToFile(ID.custom("custom_id")) // Channel helper tests - writeToFile(Channel.database()) - writeToFile(Channel.database("db1", "col1", "doc1")) - writeToFile(Channel.database("db1", "col1", "doc1", "create")) - writeToFile(Channel.tablesdb()) - writeToFile(Channel.tablesdb("db1", "table1", "row1")) - writeToFile(Channel.tablesdb("db1", "table1", "row1", "update")) + writeToFile(Channel.database().collection().document().toString()) + writeToFile(Channel.database("db1").collection("col1").document("doc1").toString()) + writeToFile(Channel.database("db1").collection("col1").document("doc1").create().toString()) + writeToFile(Channel.tablesdb().table().row().toString()) + writeToFile(Channel.tablesdb("db1").table("table1").row("row1").toString()) + writeToFile(Channel.tablesdb("db1").table("table1").row("row1").update().toString()) writeToFile(Channel.account()) writeToFile(Channel.account("user123")) - writeToFile(Channel.files()) - writeToFile(Channel.files("bucket1", "file1")) - writeToFile(Channel.files("bucket1", "file1", "delete")) - writeToFile(Channel.executions()) - writeToFile(Channel.executions("func1", "exec1")) - writeToFile(Channel.executions("func1", "exec1", "create")) - writeToFile(Channel.teams()) - writeToFile(Channel.teams("team1")) - writeToFile(Channel.teams("team1", "create")) - writeToFile(Channel.memberships()) - writeToFile(Channel.memberships("membership1")) - writeToFile(Channel.memberships("membership1", "update")) + writeToFile(Channel.buckets().file().toString()) + writeToFile(Channel.buckets("bucket1").file("file1").toString()) + writeToFile(Channel.buckets("bucket1").file("file1").delete().toString()) + writeToFile(Channel.functions().execution().toString()) + writeToFile(Channel.functions("func1").execution("exec1").toString()) + writeToFile(Channel.functions("func1").execution("exec1").create().toString()) + writeToFile(Channel.teams().toString()) + writeToFile(Channel.teams("team1").toString()) + writeToFile(Channel.teams("team1").create().toString()) + writeToFile(Channel.memberships().toString()) + writeToFile(Channel.memberships("membership1").toString()) + writeToFile(Channel.memberships("membership1").update().toString()) // Operator helper tests writeToFile(Operator.increment(1)) diff --git a/tests/languages/apple/Tests.swift b/tests/languages/apple/Tests.swift index 8917ce1b2c..50bde946c9 100644 --- a/tests/languages/apple/Tests.swift +++ b/tests/languages/apple/Tests.swift @@ -249,26 +249,26 @@ class Tests: XCTestCase { print(ID.custom("custom_id")) // Channel helper tests - print(Channel.database()) - print(Channel.database(databaseId: "db1", collectionId: "col1", documentId: "doc1")) - print(Channel.database(databaseId: "db1", collectionId: "col1", documentId: "doc1", action: "create")) - print(Channel.tablesdb()) - print(Channel.tablesdb(databaseId: "db1", tableId: "table1", rowId: "row1")) - print(Channel.tablesdb(databaseId: "db1", tableId: "table1", rowId: "row1", action: "update")) + print(Channel.database().collection().document().toString()) + print(Channel.database("db1").collection("col1").document("doc1").toString()) + print(Channel.database("db1").collection("col1").document("doc1").create().toString()) + print(Channel.tablesdb().table().row().toString()) + print(Channel.tablesdb("db1").table("table1").row("row1").toString()) + print(Channel.tablesdb("db1").table("table1").row("row1").update().toString()) print(Channel.account()) - print(Channel.account(userId: "user123")) - print(Channel.files()) - print(Channel.files(bucketId: "bucket1", fileId: "file1")) - print(Channel.files(bucketId: "bucket1", fileId: "file1", action: "delete")) - print(Channel.executions()) - print(Channel.executions(functionId: "func1", executionId: "exec1")) - print(Channel.executions(functionId: "func1", executionId: "exec1", action: "create")) - print(Channel.teams()) - print(Channel.teams(teamId: "team1")) - print(Channel.teams(teamId: "team1", action: "create")) - print(Channel.memberships()) - print(Channel.memberships(membershipId: "membership1")) - print(Channel.memberships(membershipId: "membership1", action: "update")) + print(Channel.account("user123")) + print(Channel.buckets().file().toString()) + print(Channel.buckets("bucket1").file("file1").toString()) + print(Channel.buckets("bucket1").file("file1").delete().toString()) + print(Channel.functions().execution().toString()) + print(Channel.functions("func1").execution("exec1").toString()) + print(Channel.functions("func1").execution("exec1").create().toString()) + print(Channel.teams().toString()) + print(Channel.teams("team1").toString()) + print(Channel.teams("team1").create().toString()) + print(Channel.memberships().toString()) + print(Channel.memberships("membership1").toString()) + print(Channel.memberships("membership1").update().toString()) // Operator helper tests print(Operator.increment(1)) diff --git a/tests/languages/flutter/tests.dart b/tests/languages/flutter/tests.dart index c6ab56563f..7647041c66 100644 --- a/tests/languages/flutter/tests.dart +++ b/tests/languages/flutter/tests.dart @@ -248,26 +248,26 @@ void main() async { print(ID.custom('custom_id')); // Channel helper tests - print(Channel.database()); - print(Channel.database(databaseId: 'db1', collectionId: 'col1', documentId: 'doc1')); - print(Channel.database(databaseId: 'db1', collectionId: 'col1', documentId: 'doc1', action: 'create')); - print(Channel.tablesdb()); - print(Channel.tablesdb(databaseId: 'db1', tableId: 'table1', rowId: 'row1')); - print(Channel.tablesdb(databaseId: 'db1', tableId: 'table1', rowId: 'row1', action: 'update')); + print(Channel.database().collection().document().toString()); + print(Channel.database('db1').collection('col1').document('doc1').toString()); + print(Channel.database('db1').collection('col1').document('doc1').create().toString()); + print(Channel.tablesdb().table().row().toString()); + print(Channel.tablesdb('db1').table('table1').row('row1').toString()); + print(Channel.tablesdb('db1').table('table1').row('row1').update().toString()); print(Channel.account()); - print(Channel.account(userId: 'user123')); - print(Channel.files()); - print(Channel.files(bucketId: 'bucket1', fileId: 'file1')); - print(Channel.files(bucketId: 'bucket1', fileId: 'file1', action: 'delete')); - print(Channel.executions()); - print(Channel.executions(functionId: 'func1', executionId: 'exec1')); - print(Channel.executions(functionId: 'func1', executionId: 'exec1', action: 'create')); - print(Channel.teams()); - print(Channel.teams(teamId: 'team1')); - print(Channel.teams(teamId: 'team1', action: 'create')); - print(Channel.memberships()); - print(Channel.memberships(membershipId: 'membership1')); - print(Channel.memberships(membershipId: 'membership1', action: 'update')); + print(Channel.account('user123')); + print(Channel.buckets().file().toString()); + print(Channel.buckets('bucket1').file('file1').toString()); + print(Channel.buckets('bucket1').file('file1').delete().toString()); + print(Channel.functions().execution().toString()); + print(Channel.functions('func1').execution('exec1').toString()); + print(Channel.functions('func1').execution('exec1').create().toString()); + print(Channel.teams().toString()); + print(Channel.teams('team1').toString()); + print(Channel.teams('team1').create().toString()); + print(Channel.memberships().toString()); + print(Channel.memberships('membership1').toString()); + print(Channel.memberships('membership1').update().toString()); // Operator helper tests print(Operator.increment(1)); diff --git a/tests/languages/web/index.html b/tests/languages/web/index.html index f85cbad048..e8b4d48fe1 100644 --- a/tests/languages/web/index.html +++ b/tests/languages/web/index.html @@ -320,26 +320,26 @@ console.log(ID.custom('custom_id')); // Channel helper tests - console.log(Channel.database()); - console.log(Channel.database('db1', 'col1', 'doc1')); - console.log(Channel.database('db1', 'col1', 'doc1', 'create')); - console.log(Channel.tablesdb()); - console.log(Channel.tablesdb('db1', 'table1', 'row1')); - console.log(Channel.tablesdb('db1', 'table1', 'row1', 'update')); + console.log(Channel.database().collection().document().toString()); + console.log(Channel.database('db1').collection('col1').document('doc1').toString()); + console.log(Channel.database('db1').collection('col1').document('doc1').create().toString()); + console.log(Channel.tablesdb().table().row().toString()); + console.log(Channel.tablesdb('db1').table('table1').row('row1').toString()); + console.log(Channel.tablesdb('db1').table('table1').row('row1').update().toString()); console.log(Channel.account()); console.log(Channel.account('user123')); - console.log(Channel.files()); - console.log(Channel.files('bucket1', 'file1')); - console.log(Channel.files('bucket1', 'file1', 'delete')); - console.log(Channel.executions()); - console.log(Channel.executions('func1', 'exec1')); - console.log(Channel.executions('func1', 'exec1', 'create')); - console.log(Channel.teams()); - console.log(Channel.teams('team1')); - console.log(Channel.teams('team1', 'create')); - console.log(Channel.memberships()); - console.log(Channel.memberships('membership1')); - console.log(Channel.memberships('membership1', 'update')); + console.log(Channel.buckets().file().toString()); + console.log(Channel.buckets('bucket1').file('file1').toString()); + console.log(Channel.buckets('bucket1').file('file1').delete().toString()); + console.log(Channel.functions().execution().toString()); + console.log(Channel.functions('func1').execution('exec1').toString()); + console.log(Channel.functions('func1').execution('exec1').create().toString()); + console.log(Channel.teams().toString()); + console.log(Channel.teams('team1').toString()); + console.log(Channel.teams('team1').create().toString()); + console.log(Channel.memberships().toString()); + console.log(Channel.memberships('membership1').toString()); + console.log(Channel.memberships('membership1').update().toString()); // Operator helper tests console.log(Operator.increment(1)); diff --git a/tests/languages/web/node.js b/tests/languages/web/node.js index 45bf89fee4..8e6ebd824e 100644 --- a/tests/languages/web/node.js +++ b/tests/languages/web/node.js @@ -251,26 +251,26 @@ async function start() { console.log(ID.custom('custom_id')); // Channel helper tests - console.log(Channel.database()); - console.log(Channel.database('db1', 'col1', 'doc1')); - console.log(Channel.database('db1', 'col1', 'doc1', 'create')); - console.log(Channel.tablesdb()); - console.log(Channel.tablesdb('db1', 'table1', 'row1')); - console.log(Channel.tablesdb('db1', 'table1', 'row1', 'update')); + console.log(Channel.database().collection().document().toString()); + console.log(Channel.database('db1').collection('col1').document('doc1').toString()); + console.log(Channel.database('db1').collection('col1').document('doc1').create().toString()); + console.log(Channel.tablesdb().table().row().toString()); + console.log(Channel.tablesdb('db1').table('table1').row('row1').toString()); + console.log(Channel.tablesdb('db1').table('table1').row('row1').update().toString()); console.log(Channel.account()); console.log(Channel.account('user123')); - console.log(Channel.files()); - console.log(Channel.files('bucket1', 'file1')); - console.log(Channel.files('bucket1', 'file1', 'delete')); - console.log(Channel.executions()); - console.log(Channel.executions('func1', 'exec1')); - console.log(Channel.executions('func1', 'exec1', 'create')); - console.log(Channel.teams()); - console.log(Channel.teams('team1')); - console.log(Channel.teams('team1', 'create')); - console.log(Channel.memberships()); - console.log(Channel.memberships('membership1')); - console.log(Channel.memberships('membership1', 'update')); + console.log(Channel.buckets().file().toString()); + console.log(Channel.buckets('bucket1').file('file1').toString()); + console.log(Channel.buckets('bucket1').file('file1').delete().toString()); + console.log(Channel.functions().execution().toString()); + console.log(Channel.functions('func1').execution('exec1').toString()); + console.log(Channel.functions('func1').execution('exec1').create().toString()); + console.log(Channel.teams().toString()); + console.log(Channel.teams('team1').toString()); + console.log(Channel.teams('team1').create().toString()); + console.log(Channel.memberships().toString()); + console.log(Channel.memberships('membership1').toString()); + console.log(Channel.memberships('membership1').update().toString()); // Operator helper tests console.log(Operator.increment(1)); From 3e2402d5830819767d207f009689984789de638e Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 19 Dec 2025 18:45:55 +0530 Subject: [PATCH 06/26] updated flutter config --- src/SDK/Language/Dart.php | 10 -- src/SDK/Language/Flutter.php | 2 +- templates/dart/lib/package.dart.twig | 1 - .../flutter/test/src/channel_test.dart.twig | 108 ++++++++++++++++++ 4 files changed, 109 insertions(+), 12 deletions(-) create mode 100644 templates/flutter/test/src/channel_test.dart.twig diff --git a/src/SDK/Language/Dart.php b/src/SDK/Language/Dart.php index be3067cda4..6ad8efbaa2 100644 --- a/src/SDK/Language/Dart.php +++ b/src/SDK/Language/Dart.php @@ -354,11 +354,6 @@ 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', @@ -469,11 +464,6 @@ 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', diff --git a/src/SDK/Language/Flutter.php b/src/SDK/Language/Flutter.php index 0643939a38..cfb6933ccf 100644 --- a/src/SDK/Language/Flutter.php +++ b/src/SDK/Language/Flutter.php @@ -298,7 +298,7 @@ public function getFiles(): array [ 'scope' => 'default', 'destination' => '/test/channel_test.dart', - 'template' => 'dart/test/channel_test.dart.twig', + 'template' => 'flutter/test/src/channel_test.dart.twig', ], [ 'scope' => 'default', diff --git a/templates/dart/lib/package.dart.twig b/templates/dart/lib/package.dart.twig index 43e4a2c761..bca6862a36 100644 --- a/templates/dart/lib/package.dart.twig +++ b/templates/dart/lib/package.dart.twig @@ -27,7 +27,6 @@ part 'query.dart'; part 'permission.dart'; part 'role.dart'; part 'id.dart'; -part 'channel.dart'; part 'operator.dart'; {% for service in spec.services %} part 'services/{{service.name | caseSnake}}.dart'; diff --git a/templates/flutter/test/src/channel_test.dart.twig b/templates/flutter/test/src/channel_test.dart.twig new file mode 100644 index 0000000000..f531049ec0 --- /dev/null +++ b/templates/flutter/test/src/channel_test.dart.twig @@ -0,0 +1,108 @@ +import 'package:{{ language.params.packageName }}/{{ language.params.packageName }}.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('database()', () { + test('returns database channel with defaults', () { + expect(Channel.database().collection().document().toString(), 'databases.*.collections.*.documents.*'); + }); + + test('returns database channel with specific IDs', () { + expect(Channel.database('db1').collection('col1').document('doc1').toString(), + 'databases.db1.collections.col1.documents.doc1'); + }); + + test('returns database channel with action', () { + expect(Channel.database('db1').collection('col1').document('doc1').create().toString(), + 'databases.db1.collections.col1.documents.doc1.create'); + }); + }); + + group('tablesdb()', () { + test('returns tablesdb channel with defaults', () { + expect(Channel.tablesdb().table().row().toString(), 'tablesdb.*.tables.*.rows.*'); + }); + + test('returns tablesdb channel with specific IDs', () { + expect(Channel.tablesdb('db1').table('table1').row('row1').toString(), + 'tablesdb.db1.tables.table1.rows.row1'); + }); + + test('returns tablesdb channel with action', () { + expect(Channel.tablesdb('db1').table('table1').row('row1').update().toString(), + 'tablesdb.db1.tables.table1.rows.row1.update'); + }); + }); + + group('account()', () { + test('returns account channel with default', () { + expect(Channel.account(), 'account.*'); + }); + + test('returns account channel with specific user ID', () { + expect(Channel.account('user123'), 'account.user123'); + }); + }); + + group('buckets()', () { + test('returns buckets channel with defaults', () { + expect(Channel.buckets().file().toString(), 'buckets.*.files.*'); + }); + + test('returns buckets channel with specific IDs', () { + expect(Channel.buckets('bucket1').file('file1').toString(), + 'buckets.bucket1.files.file1'); + }); + + test('returns buckets channel with action', () { + expect(Channel.buckets('bucket1').file('file1').delete().toString(), + 'buckets.bucket1.files.file1.delete'); + }); + }); + + group('functions()', () { + test('returns functions channel with defaults', () { + expect(Channel.functions().execution().toString(), 'functions.*.executions.*'); + }); + + test('returns functions channel with specific IDs', () { + expect(Channel.functions('func1').execution('exec1').toString(), + 'functions.func1.executions.exec1'); + }); + + test('returns functions channel with action', () { + expect(Channel.functions('func1').execution('exec1').create().toString(), + 'functions.func1.executions.exec1.create'); + }); + }); + + group('teams()', () { + test('returns teams channel with default', () { + expect(Channel.teams().toString(), 'teams.*'); + }); + + test('returns teams channel with specific team ID', () { + expect(Channel.teams('team1').toString(), 'teams.team1'); + }); + + test('returns teams channel with action', () { + expect(Channel.teams('team1').create().toString(), 'teams.team1.create'); + }); + }); + + group('memberships()', () { + test('returns memberships channel with default', () { + expect(Channel.memberships().toString(), 'memberships.*'); + }); + + test('returns memberships channel with specific membership ID', () { + expect(Channel.memberships('membership1').toString(), 'memberships.membership1'); + }); + + test('returns memberships channel with action', () { + expect(Channel.memberships('membership1').update().toString(), + 'memberships.membership1.update'); + }); + }); +} + From 4ff8dd6b4162a5c01b859598ac9139901220b57e Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 30 Dec 2025 17:45:48 +0530 Subject: [PATCH 07/26] Refactor channel handling and subscription logic across multiple templates - Removed deprecated channel types and streamlined channel value handling in Realtime service. - Introduced Actionable and ResolvedChannel interfaces for better type safety. - Updated database, tablesdb, buckets, functions, teams, and memberships channel builders to use new interfaces. - Enhanced test cases to reflect changes in channel creation and subscription methods. - Improved documentation for channel subscription methods to clarify expected types. --- .../src/main/java/io/package/Channel.kt.twig | 136 +++--- .../java/io/package/services/Realtime.kt.twig | 11 +- templates/apple/Sources/Channel.swift.twig | 263 +++++------ .../Sources/Services/Realtime.swift.twig | 25 +- templates/dart/test/channel_test.dart.twig | 133 ++++-- templates/flutter/lib/channel.dart.twig | 297 ++++-------- templates/flutter/lib/src/realtime.dart.twig | 2 +- .../flutter/lib/src/realtime_base.dart.twig | 2 +- .../flutter/lib/src/realtime_mixin.dart.twig | 8 + templates/react-native/src/channel.ts.twig | 306 +++++++----- templates/web/src/channel.ts.twig | 447 ++++++++---------- templates/web/src/client.ts.twig | 16 +- templates/web/src/services/realtime.ts.twig | 34 +- 13 files changed, 824 insertions(+), 856 deletions(-) diff --git a/templates/android/library/src/main/java/io/package/Channel.kt.twig b/templates/android/library/src/main/java/io/package/Channel.kt.twig index 4fe27e4324..7235456fab 100644 --- a/templates/android/library/src/main/java/io/package/Channel.kt.twig +++ b/templates/android/library/src/main/java/io/package/Channel.kt.twig @@ -1,21 +1,41 @@ package {{ sdk.namespace | caseDot }} +// The result of building the channel. class ResolvedChannel(private val value: String) { override fun toString(): String { return value } } -abstract class ActionChannel(protected val base: String) { - fun create(): ResolvedChannel { +// Helper for normalizing IDs +private fun normalize(id: String): String { + val trimmed = id.trim() + return if (trimmed.isEmpty()) "*" else trimmed +} + +// Interface for actionable channels +interface Actionable { + val channel: String + fun create(): ResolvedChannel + fun update(): ResolvedChannel + fun delete(): ResolvedChannel +} + +// Base actionable channel +abstract class ActionChannel(protected val base: String) : Actionable { + + override val channel: String + get() = base + + override fun create(): ResolvedChannel { return ResolvedChannel("$base.create") } - fun update(): ResolvedChannel { + override fun update(): ResolvedChannel { return ResolvedChannel("$base.update") } - fun delete(): ResolvedChannel { + override fun delete(): ResolvedChannel { return ResolvedChannel("$base.delete") } @@ -24,16 +44,19 @@ abstract class ActionChannel(protected val base: String) { } } +// Database → Collection → Document + class DocumentChannel(base: String) : ActionChannel(base) class CollectionChannel( - private val databaseId: String, - private val collectionId: String + databaseId: String, + collectionId: String ) { - private val base = "databases.$databaseId.collections.$collectionId" + private val base = + "databases.${normalize(databaseId)}.collections.${normalize(collectionId)}" fun document(documentId: String = "*"): DocumentChannel { - return DocumentChannel("$base.documents.$documentId") + return DocumentChannel("$base.documents.${normalize(documentId)}") } override fun toString(): String { @@ -41,7 +64,8 @@ class CollectionChannel( } } -class DatabaseChannel(private val databaseId: String) { +class DatabaseChannel(databaseId: String) { + private val databaseId = normalize(databaseId) private val base = "databases.$databaseId" fun collection(collectionId: String = "*"): CollectionChannel { @@ -53,16 +77,19 @@ class DatabaseChannel(private val databaseId: String) { } } +// TablesDB → Table → Row + class RowChannel(base: String) : ActionChannel(base) class TableChannel( - private val databaseId: String, - private val tableId: String + databaseId: String, + tableId: String ) { - private val base = "tablesdb.$databaseId.tables.$tableId" + private val base = + "tablesdb.${normalize(databaseId)}.tables.${normalize(tableId)}" fun row(rowId: String = "*"): RowChannel { - return RowChannel("$base.rows.$rowId") + return RowChannel("$base.rows.${normalize(rowId)}") } override fun toString(): String { @@ -70,7 +97,8 @@ class TableChannel( } } -class TablesDBChannel(private val databaseId: String) { +class TablesDBChannel(databaseId: String) { + private val databaseId = normalize(databaseId) private val base = "tablesdb.$databaseId" fun table(tableId: String = "*"): TableChannel { @@ -82,13 +110,16 @@ class TablesDBChannel(private val databaseId: String) { } } +// Buckets → File + class FileChannel(base: String) : ActionChannel(base) -class BucketChannel(private val bucketId: String) { +class BucketChannel(bucketId: String) { + private val bucketId = normalize(bucketId) private val base = "buckets.$bucketId" fun file(fileId: String = "*"): FileChannel { - return FileChannel("$base.files.$fileId") + return FileChannel("$base.files.${normalize(fileId)}") } override fun toString(): String { @@ -96,13 +127,16 @@ class BucketChannel(private val bucketId: String) { } } +// Functions → Execution + class ExecutionChannel(base: String) : ActionChannel(base) -class FunctionChannel(private val functionId: String) { +class FunctionChannel(functionId: String) { + private val functionId = normalize(functionId) private val base = "functions.$functionId" fun execution(executionId: String = "*"): ExecutionChannel { - return ExecutionChannel("$base.executions.$executionId") + return ExecutionChannel("$base.executions.${normalize(executionId)}") } override fun toString(): String { @@ -110,81 +144,53 @@ class FunctionChannel(private val functionId: String) { } } -class TeamChannel(teamId: String = "*") : ActionChannel("teams.$teamId") +// Teams & Memberships + +class TeamChannel(teamId: String = "*") : + ActionChannel("teams.${normalize(teamId)}") + +class MembershipChannel(membershipId: String = "*") : + ActionChannel("memberships.${normalize(membershipId)}") -class MembershipChannel(membershipId: String = "*") : ActionChannel("memberships.$membershipId") +// Entry point class Channel { companion object { - /** - * Generate a database channel builder. - * - * @param databaseId The database ID (default: "*") - * @returns DatabaseChannel - */ + fun database(databaseId: String = "*"): DatabaseChannel { return DatabaseChannel(databaseId) } - /** - * Generate a tables database channel builder. - * - * @param databaseId The database ID (default: "*") - * @returns TablesDBChannel - */ fun tablesdb(databaseId: String = "*"): TablesDBChannel { return TablesDBChannel(databaseId) } - /** - * Generate a buckets channel builder. - * - * @param bucketId The bucket ID (default: "*") - * @returns BucketChannel - */ fun buckets(bucketId: String = "*"): BucketChannel { return BucketChannel(bucketId) } - /** - * Generate a functions channel builder. - * - * @param functionId The function ID (default: "*") - * @returns FunctionChannel - */ fun functions(functionId: String = "*"): FunctionChannel { return FunctionChannel(functionId) } - /** - * Generate a teams channel builder. - * - * @param teamId The team ID (default: "*") - * @returns TeamChannel - */ fun teams(teamId: String = "*"): TeamChannel { return TeamChannel(teamId) } - /** - * Generate a memberships channel builder. - * - * @param membershipId The membership ID (default: "*") - * @returns MembershipChannel - */ fun memberships(membershipId: String = "*"): MembershipChannel { return MembershipChannel(membershipId) } - /** - * Generate an account channel string. - * - * @param userId The user ID (default: "*") - * @returns The channel string - */ - fun account(userId: String = "*"): String { - return "account.$userId" + fun account(userId: String = ""): String { + val normalized = normalize(userId) + return if (normalized == "*") "account" else "account.$normalized" } + + // Global events + + const val documents = "documents" + const val rows = "rows" + const val files = "files" + const val executions = "executions" } } - diff --git a/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig b/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig index 2e887dfe3a..79e509a788 100644 --- a/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig +++ b/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig @@ -2,6 +2,8 @@ package {{ sdk.namespace | caseDot }}.services import {{ sdk.namespace | caseDot }}.Service import {{ sdk.namespace | caseDot }}.Client +import {{ sdk.namespace | caseDot }}.Actionable +import {{ sdk.namespace | caseDot }}.ResolvedChannel import {{ sdk.namespace | caseDot }}.exceptions.{{ spec.title | caseUcfirst }}Exception import {{ sdk.namespace | caseDot }}.extensions.forEachAsync import {{ sdk.namespace | caseDot }}.extensions.fromJson @@ -111,10 +113,15 @@ class Realtime(client: Client) : Service(client), CoroutineScope { /** * Convert channel value to string + * Checks if it's an Actionable instance (use .channel), + * then ResolvedChannel (use .toString()), + * otherwise treats as string */ private fun channelToString(channel: Any): String { - return when (channel) { - is String -> channel + return when { + channel is String -> channel + channel is Actionable -> channel.channel + channel is ResolvedChannel -> channel.toString() else -> channel.toString() } } diff --git a/templates/apple/Sources/Channel.swift.twig b/templates/apple/Sources/Channel.swift.twig index 599d9d5958..3ffb7b5205 100644 --- a/templates/apple/Sources/Channel.swift.twig +++ b/templates/apple/Sources/Channel.swift.twig @@ -1,261 +1,240 @@ import Foundation -public class ResolvedChannel { +// The result of building the channel. +public final class ResolvedChannel { private let value: String - + init(value: String) { self.value = value } - + public func toString() -> String { - return value + value } } -open class ActionChannel { +// Helper for normalizing IDs +private func normalize(_ id: String) -> String { + let trimmed = id.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? "*" : trimmed +} + +// Protocol for actionable channels +public protocol Actionable { + var channel: String { get } + func create() -> ResolvedChannel + func update() -> ResolvedChannel + func delete() -> ResolvedChannel +} + +// Base actionable channel +open class ActionChannel: Actionable { let base: String + public var channel: String { + return base + } + init(base: String) { self.base = base } - + public func create() -> ResolvedChannel { - return ResolvedChannel(value: "\(base).create") + ResolvedChannel(value: "\(base).create") } - + public func update() -> ResolvedChannel { - return ResolvedChannel(value: "\(base).update") + ResolvedChannel(value: "\(base).update") } - + public func delete() -> ResolvedChannel { - return ResolvedChannel(value: "\(base).delete") + ResolvedChannel(value: "\(base).delete") } - + public func toString() -> String { - return base + base } } -/** - * ---------------------------- - * Database → Collection → Document - * ---------------------------- - */ -public class DocumentChannel: ActionChannel {} +// Database → Collection → Document + +public final class DocumentChannel: ActionChannel {} -public class CollectionChannel { +public final class CollectionChannel { private let base: String private let databaseId: String private let collectionId: String - + init(databaseId: String, collectionId: String) { - self.databaseId = databaseId - self.collectionId = collectionId - self.base = "databases.\(databaseId).collections.\(collectionId)" + self.databaseId = normalize(databaseId) + self.collectionId = normalize(collectionId) + self.base = "databases.\(self.databaseId).collections.\(self.collectionId)" } - + public func document(_ documentId: String = "*") -> DocumentChannel { - return DocumentChannel(base: "\(base).documents.\(documentId)") + DocumentChannel(base: "\(base).documents.\(normalize(documentId))") } - + public func toString() -> String { - return base + base } } -public class DatabaseChannel { +public final class DatabaseChannel { private let base: String private let databaseId: String - + init(databaseId: String) { - self.databaseId = databaseId - self.base = "databases.\(databaseId)" + self.databaseId = normalize(databaseId) + self.base = "databases.\(self.databaseId)" } - + public func collection(_ collectionId: String = "*") -> CollectionChannel { - return CollectionChannel(databaseId: databaseId, collectionId: collectionId) + CollectionChannel(databaseId: databaseId, collectionId: collectionId) } - + public func toString() -> String { - return base + base } } -/** - * ---------------------------- - * TablesDB → Table → Row - * ---------------------------- - */ -public class RowChannel: ActionChannel {} +// TablesDB → Table → Row + +public final class RowChannel: ActionChannel {} -public class TableChannel { +public final class TableChannel { private let base: String private let databaseId: String private let tableId: String - + init(databaseId: String, tableId: String) { - self.databaseId = databaseId - self.tableId = tableId - self.base = "tablesdb.\(databaseId).tables.\(tableId)" + self.databaseId = normalize(databaseId) + self.tableId = normalize(tableId) + self.base = "tablesdb.\(self.databaseId).tables.\(self.tableId)" } - + public func row(_ rowId: String = "*") -> RowChannel { - return RowChannel(base: "\(base).rows.\(rowId)") + RowChannel(base: "\(base).rows.\(normalize(rowId))") } - + public func toString() -> String { - return base + base } } -public class TablesDBChannel { +public final class TablesDBChannel { private let base: String private let databaseId: String - + init(databaseId: String) { - self.databaseId = databaseId - self.base = "tablesdb.\(databaseId)" + self.databaseId = normalize(databaseId) + self.base = "tablesdb.\(self.databaseId)" } - + public func table(_ tableId: String = "*") -> TableChannel { - return TableChannel(databaseId: databaseId, tableId: tableId) + TableChannel(databaseId: databaseId, tableId: tableId) } - + public func toString() -> String { - return base + base } } -/** - * ---------------------------- - * Buckets → File - * ---------------------------- - */ -public class FileChannel: ActionChannel {} +// Buckets → File -public class BucketChannel { +public final class FileChannel: ActionChannel {} + +public final class BucketChannel { private let base: String private let bucketId: String - + init(bucketId: String) { - self.bucketId = bucketId - self.base = "buckets.\(bucketId)" + self.bucketId = normalize(bucketId) + self.base = "buckets.\(self.bucketId)" } - + public func file(_ fileId: String = "*") -> FileChannel { - return FileChannel(base: "\(base).files.\(fileId)") + FileChannel(base: "\(base).files.\(normalize(fileId))") } - + public func toString() -> String { - return base + base } } -/** - * ---------------------------- - * Functions → Execution - * ---------------------------- - */ -public class ExecutionChannel: ActionChannel {} +// Functions → Execution + +public final class ExecutionChannel: ActionChannel {} -public class FunctionChannel { +public final class FunctionChannel { private let base: String private let functionId: String - + init(functionId: String) { - self.functionId = functionId - self.base = "functions.\(functionId)" + self.functionId = normalize(functionId) + self.base = "functions.\(self.functionId)" } - + public func execution(_ executionId: String = "*") -> ExecutionChannel { - return ExecutionChannel(base: "\(base).executions.\(executionId)") + ExecutionChannel(base: "\(base).executions.\(normalize(executionId))") } - + public func toString() -> String { - return base + base } } -public class TeamChannel: ActionChannel { +// Teams & Memberships + +public final class TeamChannel: ActionChannel { init(teamId: String = "*") { - super.init(base: "teams.\(teamId)") + super.init(base: "teams.\(normalize(teamId))") } } -public class MembershipChannel: ActionChannel { +public final class MembershipChannel: ActionChannel { init(membershipId: String = "*") { - super.init(base: "memberships.\(membershipId)") + super.init(base: "memberships.\(normalize(membershipId))") } } -public class Channel { - /** - * Generate a database channel builder. - * - * @param databaseId The database ID (default: "*") - * @returns DatabaseChannel - */ +// Entry point + +public final class Channel { + public static func database(_ databaseId: String = "*") -> DatabaseChannel { - return DatabaseChannel(databaseId: databaseId) + DatabaseChannel(databaseId: databaseId) } - /** - * Generate a tables database channel builder. - * - * @param databaseId The database ID (default: "*") - * @returns TablesDBChannel - */ public static func tablesdb(_ databaseId: String = "*") -> TablesDBChannel { - return TablesDBChannel(databaseId: databaseId) + TablesDBChannel(databaseId: databaseId) } - /** - * Generate a buckets channel builder. - * - * @param bucketId The bucket ID (default: "*") - * @returns BucketChannel - */ public static func buckets(_ bucketId: String = "*") -> BucketChannel { - return BucketChannel(bucketId: bucketId) + BucketChannel(bucketId: bucketId) } - /** - * Generate a functions channel builder. - * - * @param functionId The function ID (default: "*") - * @returns FunctionChannel - */ public static func functions(_ functionId: String = "*") -> FunctionChannel { - return FunctionChannel(functionId: functionId) + FunctionChannel(functionId: functionId) } - /** - * Generate a teams channel builder. - * - * @param teamId The team ID (default: "*") - * @returns TeamChannel - */ public static func teams(_ teamId: String = "*") -> TeamChannel { - return TeamChannel(teamId: teamId) + TeamChannel(teamId: teamId) } - /** - * Generate a memberships channel builder. - * - * @param membershipId The membership ID (default: "*") - * @returns MembershipChannel - */ public static func memberships(_ membershipId: String = "*") -> MembershipChannel { - return MembershipChannel(membershipId: membershipId) + MembershipChannel(membershipId: membershipId) } - /** - * 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)" + public static func account(_ userId: String = "") -> String { + let normalized = normalize(userId) + return normalized == "*" ? "account" : "account.\(normalized)" } + + // Global events + + public static let documents = "documents" + public static let rows = "rows" + public static let files = "files" + public static let executions = "executions" } diff --git a/templates/apple/Sources/Services/Realtime.swift.twig b/templates/apple/Sources/Services/Realtime.swift.twig index a66f009595..c880b226a1 100644 --- a/templates/apple/Sources/Services/Realtime.swift.twig +++ b/templates/apple/Sources/Services/Realtime.swift.twig @@ -17,18 +17,7 @@ extension String: ChannelValue { } extension ResolvedChannel: ChannelValue {} -extension DatabaseChannel: ChannelValue {} -extension CollectionChannel: ChannelValue {} -extension DocumentChannel: ChannelValue {} -extension TablesDBChannel: ChannelValue {} -extension TableChannel: ChannelValue {} -extension RowChannel: ChannelValue {} -extension BucketChannel: ChannelValue {} -extension FileChannel: ChannelValue {} -extension FunctionChannel: ChannelValue {} -extension ExecutionChannel: ChannelValue {} -extension TeamChannel: ChannelValue {} -extension MembershipChannel: ChannelValue {} +extension ActionChannel: ChannelValue {} open class Realtime : Service { @@ -146,8 +135,20 @@ open class Realtime : Service { /** * Convert channel value to string + * Checks if it's an Actionable instance (use .channel), + * then ResolvedChannel (use .toString()), + * otherwise treats as string */ private func channelToString(_ channel: ChannelValue) -> String { + // Check if it's an Actionable instance (has channel property) + if let actionable = channel as? Actionable { + return actionable.channel + } + // Check if it's a ResolvedChannel (has toString() method) + if let resolved = channel as? ResolvedChannel { + return resolved.toString() + } + // Otherwise use toString() (works for strings and other channel types) return channel.toString() } diff --git a/templates/dart/test/channel_test.dart.twig b/templates/dart/test/channel_test.dart.twig index db3cf04fe3..1116d1ac73 100644 --- a/templates/dart/test/channel_test.dart.twig +++ b/templates/dart/test/channel_test.dart.twig @@ -7,105 +7,152 @@ import 'package:flutter_test/flutter_test.dart'; void main() { group('database()', () { - test('returns database channel with defaults', () { - expect(Channel.database(), 'databases.*.collections.*.documents.*'); + test('returns database channel builder', () { + final builder = Channel.database(); + expect(builder.channel, 'databases.*'); }); - test('returns database channel with specific IDs', () { - expect(Channel.database(databaseId: 'db1', collectionId: 'col1', documentId: 'doc1'), - 'databases.db1.collections.col1.documents.doc1'); + test('returns database channel with specific ID', () { + final builder = Channel.database('db1'); + expect(builder.channel, 'databases.db1'); + }); + + test('returns database channel with collection and document', () { + final channel = Channel.database('db1').collection('col1').document('doc1'); + expect(channel.channel, 'databases.db1.collections.col1.documents.doc1'); }); test('returns database channel with action', () { - expect(Channel.database(databaseId: 'db1', collectionId: 'col1', documentId: 'doc1', action: 'create'), - 'databases.db1.collections.col1.documents.doc1.create'); + final channel = Channel.database('db1').collection('col1').document('doc1').create(); + expect(channel.toString(), 'databases.db1.collections.col1.documents.doc1.create'); }); }); group('tablesdb()', () { - test('returns tablesdb channel with defaults', () { - expect(Channel.tablesdb(), 'tablesdb.*.tables.*.rows.*'); + test('returns tablesdb channel builder', () { + final builder = Channel.tablesdb(); + expect(builder.channel, 'tablesdb.*'); }); - test('returns tablesdb channel with specific IDs', () { - expect(Channel.tablesdb(databaseId: 'db1', tableId: 'table1', rowId: 'row1'), - 'tablesdb.db1.tables.table1.rows.row1'); + test('returns tablesdb channel with specific ID', () { + final builder = Channel.tablesdb('db1'); + expect(builder.channel, 'tablesdb.db1'); + }); + + test('returns tablesdb channel with table and row', () { + final channel = Channel.tablesdb('db1').table('table1').row('row1'); + expect(channel.channel, 'tablesdb.db1.tables.table1.rows.row1'); }); test('returns tablesdb channel with action', () { - expect(Channel.tablesdb(databaseId: 'db1', tableId: 'table1', rowId: 'row1', action: 'update'), - 'tablesdb.db1.tables.table1.rows.row1.update'); + final channel = Channel.tablesdb('db1').table('table1').row('row1').update(); + expect(channel.toString(), 'tablesdb.db1.tables.table1.rows.row1.update'); }); }); group('account()', () { test('returns account channel with default', () { - expect(Channel.account(), 'account.*'); + expect(Channel.account(), 'account'); }); test('returns account channel with specific user ID', () { - expect(Channel.account(userId: 'user123'), 'account.user123'); + expect(Channel.account('user123'), 'account.user123'); }); }); - group('files()', () { - test('returns files channel with defaults', () { - expect(Channel.files(), 'buckets.*.files.*'); + group('buckets()', () { + test('returns buckets channel builder', () { + final builder = Channel.buckets(); + expect(builder.channel, 'buckets.*'); }); - test('returns files channel with specific IDs', () { - expect(Channel.files(bucketId: 'bucket1', fileId: 'file1'), - 'buckets.bucket1.files.file1'); + test('returns buckets channel with specific ID', () { + final builder = Channel.buckets('bucket1'); + expect(builder.channel, 'buckets.bucket1'); }); - test('returns files channel with action', () { - expect(Channel.files(bucketId: 'bucket1', fileId: 'file1', action: 'delete'), - 'buckets.bucket1.files.file1.delete'); + test('returns buckets channel with file', () { + final channel = Channel.buckets('bucket1').file('file1'); + expect(channel.channel, 'buckets.bucket1.files.file1'); + }); + + test('returns buckets channel with action', () { + final channel = Channel.buckets('bucket1').file('file1').delete(); + expect(channel.toString(), 'buckets.bucket1.files.file1.delete'); }); }); - group('executions()', () { - test('returns executions channel with defaults', () { - expect(Channel.executions(), 'functions.*.executions.*'); + group('functions()', () { + test('returns functions channel builder', () { + final builder = Channel.functions(); + expect(builder.channel, 'functions.*'); + }); + + test('returns functions channel with specific ID', () { + final builder = Channel.functions('func1'); + expect(builder.channel, 'functions.func1'); }); - test('returns executions channel with specific IDs', () { - expect(Channel.executions(functionId: 'func1', executionId: 'exec1'), - 'functions.func1.executions.exec1'); + test('returns functions channel with execution', () { + final channel = Channel.functions('func1').execution('exec1'); + expect(channel.channel, 'functions.func1.executions.exec1'); }); - test('returns executions channel with action', () { - expect(Channel.executions(functionId: 'func1', executionId: 'exec1', action: 'create'), - 'functions.func1.executions.exec1.create'); + test('returns functions channel with action', () { + final channel = Channel.functions('func1').execution('exec1').create(); + expect(channel.toString(), 'functions.func1.executions.exec1.create'); }); }); group('teams()', () { - test('returns teams channel with default', () { - expect(Channel.teams(), 'teams.*'); + test('returns teams channel builder', () { + final builder = Channel.teams(); + expect(builder.channel, 'teams.*'); }); test('returns teams channel with specific team ID', () { - expect(Channel.teams(teamId: 'team1'), 'teams.team1'); + final builder = Channel.teams('team1'); + expect(builder.channel, 'teams.team1'); }); test('returns teams channel with action', () { - expect(Channel.teams(teamId: 'team1', action: 'create'), 'teams.team1.create'); + final channel = Channel.teams('team1').create(); + expect(channel.toString(), 'teams.team1.create'); }); }); group('memberships()', () { - test('returns memberships channel with default', () { - expect(Channel.memberships(), 'memberships.*'); + test('returns memberships channel builder', () { + final builder = Channel.memberships(); + expect(builder.channel, 'memberships.*'); }); test('returns memberships channel with specific membership ID', () { - expect(Channel.memberships(membershipId: 'membership1'), 'memberships.membership1'); + final builder = Channel.memberships('membership1'); + expect(builder.channel, 'memberships.membership1'); }); test('returns memberships channel with action', () { - expect(Channel.memberships(membershipId: 'membership1', action: 'update'), - 'memberships.membership1.update'); + final channel = Channel.memberships('membership1').update(); + expect(channel.toString(), 'memberships.membership1.update'); + }); + }); + + group('global events', () { + test('returns documents channel', () { + expect(Channel.documents, 'documents'); + }); + + test('returns rows channel', () { + expect(Channel.rows, 'rows'); + }); + + test('returns files channel', () { + expect(Channel.files, 'files'); + }); + + test('returns executions channel', () { + expect(Channel.executions, 'executions'); }); }); } \ No newline at end of file diff --git a/templates/flutter/lib/channel.dart.twig b/templates/flutter/lib/channel.dart.twig index 1515f01f30..a80c18def5 100644 --- a/templates/flutter/lib/channel.dart.twig +++ b/templates/flutter/lib/channel.dart.twig @@ -1,239 +1,138 @@ -part of '{{ language.params.packageName }}.dart'; +part of appwrite_channel; -class ResolvedChannel { - final String _value; +// The result of building the channel. +final class ResolvedChannel { + final String value; + const ResolvedChannel(this.value); - ResolvedChannel(this._value); - - String toString() { - return _value; - } + @override + String toString() => value; } -abstract class ActionChannel { - final String base; +// Helper function for normalizing ID +String _normalize(String id) => id.trim().isEmpty ? '*' : id; - ActionChannel(this.base); - - ResolvedChannel create() { - return ResolvedChannel('$base.create'); - } - - ResolvedChannel update() { - return ResolvedChannel('$base.update'); - } - - ResolvedChannel delete() { - return ResolvedChannel('$base.delete'); - } +// State interfaces - @override - String toString() { - return base; - } +abstract interface class Actionable { + ResolvedChannel create(); + ResolvedChannel update(); + ResolvedChannel delete(); + String get channel; } -/** - * ---------------------------- - * Database → Collection → Document - * ---------------------------- - */ -class DocumentChannel extends ActionChannel { - DocumentChannel(String base) : super(base); +abstract interface class IDatabase { + ICollection collection([String id = '*']); } -class CollectionChannel { - final String base; - final String databaseId; - final String collectionId; - - CollectionChannel(this.databaseId, this.collectionId) - : base = 'databases.$databaseId.collections.$collectionId'; - - DocumentChannel document([String documentId = '*']) { - return DocumentChannel('$base.documents.$documentId'); - } - - @override - String toString() { - return base; - } +abstract interface class ICollection { + IDocument document([String id = '*']); } -class DatabaseChannel { - final String base; - final String databaseId; +abstract interface class IDocument implements Actionable {} - DatabaseChannel(this.databaseId) : base = 'databases.$databaseId'; - - CollectionChannel collection([String collectionId = '*']) { - return CollectionChannel(databaseId, collectionId); - } - - @override - String toString() { - return base; - } +abstract interface class ITablesDB { + ITable table([String id = '*']); } -/** - * ---------------------------- - * TablesDB → Table → Row - * ---------------------------- - */ -class RowChannel extends ActionChannel { - RowChannel(String base) : super(base); +abstract interface class ITable { + IRow row([String id = '*']); } -class TableChannel { - final String base; - final String databaseId; - final String tableId; - - TableChannel(this.databaseId, this.tableId) - : base = 'tablesdb.$databaseId.tables.$tableId'; - - RowChannel row([String rowId = '*']) { - return RowChannel('$base.rows.$rowId'); - } +abstract interface class IRow implements Actionable {} - @override - String toString() { - return base; - } +abstract interface class IBucket { + IFile file([String id = '*']); } -class TablesDBChannel { - final String base; - final String databaseId; - - TablesDBChannel(this.databaseId) : base = 'tablesdb.$databaseId'; - - TableChannel table([String tableId = '*']) { - return TableChannel(databaseId, tableId); - } +abstract interface class IFile implements Actionable {} - @override - String toString() { - return base; - } +abstract interface class IFunction { + IExecution execution([String id = '*']); } -/** - * ---------------------------- - * Buckets → File - * ---------------------------- - */ -class FileChannel extends ActionChannel { - FileChannel(String base) : super(base); -} +abstract interface class IExecution implements Actionable {} -class BucketChannel { - final String base; - final String bucketId; +// Builder implementation - BucketChannel(this.bucketId) : base = 'buckets.$bucketId'; +class _ChannelBuilder implements + IDatabase, ICollection, IDocument, + ITablesDB, ITable, IRow, + IBucket, IFile, + IFunction, IExecution, Actionable { + + final List _segments; - FileChannel file([String fileId = '*']) { - return FileChannel('$base.files.$fileId'); - } - - @override - String toString() { - return base; - } -} - -/** - * ---------------------------- - * Functions → Execution - * ---------------------------- - */ -class ExecutionChannel extends ActionChannel { - ExecutionChannel(String base) : super(base); -} - -class FunctionChannel { - final String base; - final String functionId; + _ChannelBuilder._(this._segments); - FunctionChannel(this.functionId) : base = 'functions.$functionId'; - - ExecutionChannel execution([String executionId = '*']) { - return ExecutionChannel('$base.executions.$executionId'); + _ChannelBuilder _next(String segment, String id) { + return _ChannelBuilder._([ + ..._segments, + segment, + _normalize(id), + ]); } @override - String toString() { - return base; - } -} - -class TeamChannel extends ActionChannel { - TeamChannel([String teamId = '*']) : super('teams.$teamId'); -} + ICollection collection([String id = '*']) => _next('collections', id); + + @override + IDocument document([String id = '*']) => _next('documents', id); -class MembershipChannel extends ActionChannel { - MembershipChannel([String membershipId = '*']) - : super('memberships.$membershipId'); -} + @override + ITable table([String id = '*']) => _next('tables', id); -typedef ChannelValue = Object; + @override + IRow row([String id = '*']) => _next('rows', id); -class Channel { - /// Generate a database channel builder. - /// - /// [databaseId] The database ID (default: "*") - /// Returns [DatabaseChannel] - static DatabaseChannel database([String databaseId = '*']) { - return DatabaseChannel(databaseId); - } + @override + IFile file([String id = '*']) => _next('files', id); - /// Generate a tables database channel builder. - /// - /// [databaseId] The database ID (default: "*") - /// Returns [TablesDBChannel] - static TablesDBChannel tablesdb([String databaseId = '*']) { - return TablesDBChannel(databaseId); - } + @override + IExecution execution([String id = '*']) => _next('executions', id); - /// Generate a buckets channel builder. - /// - /// [bucketId] The bucket ID (default: "*") - /// Returns [BucketChannel] - static BucketChannel buckets([String bucketId = '*']) { - return BucketChannel(bucketId); - } + @override + ResolvedChannel create() => _resolve('create'); + @override + ResolvedChannel update() => _resolve('update'); + @override + ResolvedChannel delete() => _resolve('delete'); - /// Generate a functions channel builder. - /// - /// [functionId] The function ID (default: "*") - /// Returns [FunctionChannel] - static FunctionChannel functions([String functionId = '*']) { - return FunctionChannel(functionId); - } + @override + String get channel => _segments.join('.'); - /// Generate a teams channel builder. - /// - /// [teamId] The team ID (default: "*") - /// Returns [TeamChannel] - static TeamChannel teams([String teamId = '*']) { - return TeamChannel(teamId); - } + @override + String toString() => channel; - /// Generate a memberships channel builder. - /// - /// [membershipId] The membership ID (default: "*") - /// Returns [MembershipChannel] - static MembershipChannel memberships([String membershipId = '*']) { - return MembershipChannel(membershipId); - } + ResolvedChannel _resolve(String action) => ResolvedChannel('$channel.$action'); +} - /// Generate an account channel string. - /// - /// [userId] The user ID (default: "*") - /// Returns The channel string - static String account([String userId = '*']) { - return 'account.$userId'; - } +// Entry point + +abstract final class Channel { + static IDatabase database([String id = '*']) => + _ChannelBuilder._(['databases', _normalize(id)]); + + static ITablesDB tablesdb([String id = '*']) => + _ChannelBuilder._(['tablesdb', _normalize(id)]); + + static IBucket buckets([String id = '*']) => + _ChannelBuilder._(['buckets', _normalize(id)]); + + static IFunction functions([String id = '*']) => + _ChannelBuilder._(['functions', _normalize(id)]); + + static Actionable teams([String id = '*']) => + _ChannelBuilder._(['teams', _normalize(id)]); + + static Actionable memberships([String id = '*']) => + _ChannelBuilder._(['memberships', _normalize(id)]); + + static String account([String userId = '']) => + userId.isEmpty ? 'account' : 'account.${_normalize(userId)}'; + // Global events (without ID) + static String get documents => 'documents'; + static String get rows => 'rows'; + static String get files => 'files'; + static String get executions => 'executions'; } \ No newline at end of file diff --git a/templates/flutter/lib/src/realtime.dart.twig b/templates/flutter/lib/src/realtime.dart.twig index 5cf072a168..fef7cc0d7a 100644 --- a/templates/flutter/lib/src/realtime.dart.twig +++ b/templates/flutter/lib/src/realtime.dart.twig @@ -50,7 +50,7 @@ abstract class Realtime extends Service { /// 'account.*' /// ]); /// ``` - RealtimeSubscription subscribe(List channels); + RealtimeSubscription subscribe(List channels); // Object can be String, Actionable, or ResolvedChannel /// The [close code](https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5) set when the WebSocket connection is closed. /// diff --git a/templates/flutter/lib/src/realtime_base.dart.twig b/templates/flutter/lib/src/realtime_base.dart.twig index 4793110742..10c7a59098 100644 --- a/templates/flutter/lib/src/realtime_base.dart.twig +++ b/templates/flutter/lib/src/realtime_base.dart.twig @@ -3,5 +3,5 @@ import 'realtime.dart'; abstract class RealtimeBase implements Realtime { @override - RealtimeSubscription subscribe(List channels); + RealtimeSubscription subscribe(List channels); // Object can be String, Actionable, or ResolvedChannel } diff --git a/templates/flutter/lib/src/realtime_mixin.dart.twig b/templates/flutter/lib/src/realtime_mixin.dart.twig index e34bc1a9c0..f78de3bdb6 100644 --- a/templates/flutter/lib/src/realtime_mixin.dart.twig +++ b/templates/flutter/lib/src/realtime_mixin.dart.twig @@ -173,6 +173,14 @@ mixin RealtimeMixin { if (channel is String) { return channel; } + // Check if it's an Actionable instance (has channel getter) + if (channel is Actionable) { + return channel.channel; + } + // Check if it's a ResolvedChannel (has toString() method) + if (channel is ResolvedChannel) { + return channel.toString(); + } return channel.toString(); } diff --git a/templates/react-native/src/channel.ts.twig b/templates/react-native/src/channel.ts.twig index fe2cada9e6..b22874b88e 100644 --- a/templates/react-native/src/channel.ts.twig +++ b/templates/react-native/src/channel.ts.twig @@ -1,111 +1,195 @@ -/** - * Helper class to generate channel strings for realtime subscriptions. - */ -export class Channel { - /** - * Generate a database channel string. - * - * @param {string} databaseId - * @param {string} collectionId - * @param {string} documentId - * @param {'create'|'update'|'delete'|null} action - * @returns {string} - */ - static database(databaseId: string = '*', collectionId: string = '*', documentId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { - let channel = `databases.${databaseId}.collections.${collectionId}.documents.${documentId}`; - if (action) { - channel += `.${action}`; - } - return channel; - } - - /** - * Generate a tables database channel string. - * - * @param {string} databaseId - * @param {string} tableId - * @param {string} rowId - * @param {'create'|'update'|'delete'|null} action - * @returns {string} - */ - static tablesdb(databaseId: string = '*', tableId: string = '*', rowId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { - let channel = `tablesdb.${databaseId}.tables.${tableId}.rows.${rowId}`; - if (action) { - channel += `.${action}`; - } - return channel; - } - - /** - * Generate an account channel string. - * - * @param {string} userId - * @returns {string} - */ - static account(userId: string = '*'): string { - return `account.${userId}`; - } - - /** - * Generate a files channel string. - * - * @param {string} bucketId - * @param {string} fileId - * @param {'create'|'update'|'delete'|null} action - * @returns {string} - */ - static files(bucketId: string = '*', fileId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { - let channel = `buckets.${bucketId}.files.${fileId}`; - if (action) { - channel += `.${action}`; - } - return channel; - } - - /** - * Generate an executions channel string. - * - * @param {string} functionId - * @param {string} executionId - * @param {'create'|'update'|'delete'|null} action - * @returns {string} - */ - static executions(functionId: string = '*', executionId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { - let channel = `functions.${functionId}.executions.${executionId}`; - if (action) { - channel += `.${action}`; - } - return channel; - } - - /** - * Generate a teams channel string. - * - * @param {string} teamId - * @param {'create'|'update'|'delete'|null} action - * @returns {string} - */ - static teams(teamId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { - let channel = `teams.${teamId}`; - if (action) { - channel += `.${action}`; - } - return channel; - } - - /** - * Generate a memberships channel string. - * - * @param {string} membershipId - * @param {'create'|'update'|'delete'|null} action - * @returns {string} - */ - static memberships(membershipId: string = '*', action: 'create' | 'update' | 'delete' | null = null): string { - let channel = `memberships.${membershipId}`; - if (action) { - channel += `.${action}`; - } - return channel; - } -} - +// The result of building the channel. +class ResolvedChannel { + constructor(private value: string) {} + toString() { + return this.value; + } + } + + // Helper function for normalizing ID + function normalize(id: string): string { + const trimmed = id.trim(); + return trimmed === "" ? "*" : trimmed; + } + + // State interfaces + + interface Actionable { + create(): ResolvedChannel; + update(): ResolvedChannel; + delete(): ResolvedChannel; + channel(): string; + } + + interface IDatabase { + collection(id?: string): ICollection; + } + + interface ICollection { + document(id?: string): IDocument; + } + + interface IDocument extends Actionable {} + + interface ITablesDB { + table(id?: string): ITable; + } + + interface ITable { + row(id?: string): IRow; + } + + interface IRow extends Actionable {} + + interface IBucket { + file(id?: string): IFile; + } + + interface IFile extends Actionable {} + + interface IFunction { + execution(id?: string): IExecution; + } + + interface IExecution extends Actionable {} + + // Builder implementation + + class ChannelBuilder + implements + IDatabase, + ICollection, + IDocument, + ITablesDB, + ITable, + IRow, + IBucket, + IFile, + IFunction, + IExecution, + Actionable + { + constructor(private segments: string[]) {} + + static start(segments: string[]): ChannelBuilder { + return new ChannelBuilder(segments); + } + + channel(): string { + return this.segments.join("."); + } + + toString(): string { + return this.channel(); + } + + private next(segment: string, id: string = "*"): ChannelBuilder { + return new ChannelBuilder([ + ...this.segments, + segment, + normalize(id), + ]); + } + + private resolve(action: string): ResolvedChannel { + return new ResolvedChannel(`${this.channel()}.${action}`); + } + + // Database + + collection(id: string = "*"): ICollection { + return this.next("collections", id); + } + + document(id: string = "*"): IDocument { + return this.next("documents", id); + } + + // TablesDB + + table(id: string = "*"): ITable { + return this.next("tables", id); + } + + row(id: string = "*"): IRow { + return this.next("rows", id); + } + + // Buckets + + file(id: string = "*"): IFile { + return this.next("files", id); + } + + // Functions + + execution(id: string = "*"): IExecution { + return this.next("executions", id); + } + + // Actions + + create(): ResolvedChannel { + return this.resolve("create"); + } + + update(): ResolvedChannel { + return this.resolve("update"); + } + + delete(): ResolvedChannel { + return this.resolve("delete"); + } + } + + // Entry point + + export class Channel { + static database(id: string = "*"): IDatabase { + return ChannelBuilder.start(["databases", normalize(id)]); + } + + static tablesdb(id: string = "*"): ITablesDB { + return ChannelBuilder.start(["tablesdb", normalize(id)]); + } + + static buckets(id: string = "*"): IBucket { + return ChannelBuilder.start(["buckets", normalize(id)]); + } + + static functions(id: string = "*"): IFunction { + return ChannelBuilder.start(["functions", normalize(id)]); + } + + static teams(id: string = "*"): Actionable { + return ChannelBuilder.start(["teams", normalize(id)]); + } + + static memberships(id: string = "*"): Actionable { + return ChannelBuilder.start(["memberships", normalize(id)]); + } + + static account(userId: string = ""): string { + return userId.trim() + ? `account.${normalize(userId)}` + : "account"; + } + + // Global events + + static get documents(): string { + return "documents"; + } + + static get rows(): string { + return "rows"; + } + + static get files(): string { + return "files"; + } + + static get executions(): string { + return "executions"; + } + } diff --git a/templates/web/src/channel.ts.twig b/templates/web/src/channel.ts.twig index ac14ebdf87..f4fefdeb7a 100644 --- a/templates/web/src/channel.ts.twig +++ b/templates/web/src/channel.ts.twig @@ -1,266 +1,195 @@ - - +// The result of building the channel. export class ResolvedChannel { - private value: string; - - constructor(value: string) { - this.value = value; - } - + constructor(private value: string) {} + toString() { + return this.value; + } + } + + // Helper function for normalizing ID + function normalize(id: string): string { + const trimmed = id.trim(); + return trimmed === "" ? "*" : trimmed; + } + + // State interfaces + + export interface Actionable { + create(): ResolvedChannel; + update(): ResolvedChannel; + delete(): ResolvedChannel; + channel(): string; + } + + interface IDatabase { + collection(id?: string): ICollection; + } + + interface ICollection { + document(id?: string): IDocument; + } + + interface IDocument extends Actionable {} + + interface ITablesDB { + table(id?: string): ITable; + } + + interface ITable { + row(id?: string): IRow; + } + + interface IRow extends Actionable {} + + interface IBucket { + file(id?: string): IFile; + } + + interface IFile extends Actionable {} + + interface IFunction { + execution(id?: string): IExecution; + } + + interface IExecution extends Actionable {} + + // Builder implementation + + class ChannelBuilder + implements + IDatabase, + ICollection, + IDocument, + ITablesDB, + ITable, + IRow, + IBucket, + IFile, + IFunction, + IExecution, + Actionable + { + constructor(private segments: string[]) {} + + static start(segments: string[]): ChannelBuilder { + return new ChannelBuilder(segments); + } + + channel(): string { + return this.segments.join("."); + } + toString(): string { - return this.value; - } -} - -abstract class ActionChannel { - protected base: string; - - constructor(base: string) { - this.base = base; - } - + return this.channel(); + } + + private next(segment: string, id: string = "*"): ChannelBuilder { + return new ChannelBuilder([ + ...this.segments, + segment, + normalize(id), + ]); + } + + private resolve(action: string): ResolvedChannel { + return new ResolvedChannel(`${this.channel()}.${action}`); + } + + // Database + + collection(id: string = "*"): ICollection { + return this.next("collections", id); + } + + document(id: string = "*"): IDocument { + return this.next("documents", id); + } + + // TablesDB + + table(id: string = "*"): ITable { + return this.next("tables", id); + } + + row(id: string = "*"): IRow { + return this.next("rows", id); + } + + // Buckets + + file(id: string = "*"): IFile { + return this.next("files", id); + } + + // Functions + + execution(id: string = "*"): IExecution { + return this.next("executions", id); + } + + // Actions + create(): ResolvedChannel { - return new ResolvedChannel(`${this.base}.create`); + return this.resolve("create"); } - + update(): ResolvedChannel { - return new ResolvedChannel(`${this.base}.update`); + return this.resolve("update"); } - + delete(): ResolvedChannel { - return new ResolvedChannel(`${this.base}.delete`); - } - - toString(): string { - return this.base; - } -} - -/** - * ---------------------------- - * Database → Collection → Document - * ---------------------------- - */ -class DocumentChannel extends ActionChannel {} - -class CollectionChannel { - private base: string; - - constructor( - private databaseId: string, - private collectionId: string - ) { - this.base = `databases.${databaseId}.collections.${collectionId}`; - } - - document(documentId: string = '*'): DocumentChannel { - return new DocumentChannel( - `${this.base}.documents.${documentId}` - ); - } - - toString(): string { - return this.base; - } -} - -class DatabaseChannel { - private base: string; - - constructor(private databaseId: string) { - this.base = `databases.${databaseId}`; - } - - collection(collectionId: string = '*'): CollectionChannel { - return new CollectionChannel(this.databaseId, collectionId); - } - - toString(): string { - return this.base; - } -} - -/** - * ---------------------------- - * TablesDB → Table → Row - * ---------------------------- - */ -class RowChannel extends ActionChannel {} - -class TableChannel { - private base: string; - - constructor( - private databaseId: string, - private tableId: string - ) { - this.base = `tablesdb.${databaseId}.tables.${tableId}`; - } - - row(rowId: string = '*'): RowChannel { - return new RowChannel( - `${this.base}.rows.${rowId}` - ); - } - - toString(): string { - return this.base; - } -} - -class TablesDBChannel { - private base: string; - - constructor(private databaseId: string) { - this.base = `tablesdb.${databaseId}`; - } - - table(tableId: string = '*'): TableChannel { - return new TableChannel(this.databaseId, tableId); - } - - toString(): string { - return this.base; - } -} - -/** - * ---------------------------- - * Buckets → File - * ---------------------------- - */ -class FileChannel extends ActionChannel {} - -class BucketChannel { - private base: string; - - constructor(private bucketId: string) { - this.base = `buckets.${bucketId}`; - } - - file(fileId: string = '*'): FileChannel { - return new FileChannel( - `${this.base}.files.${fileId}` - ); - } - - toString(): string { - return this.base; - } -} - -/** - * ---------------------------- - * Functions → Execution - * ---------------------------- - */ -class ExecutionChannel extends ActionChannel {} - -class FunctionChannel { - private base: string; - - constructor(private functionId: string) { - this.base = `functions.${functionId}`; - } - - execution(executionId: string = '*'): ExecutionChannel { - return new ExecutionChannel( - `${this.base}.executions.${executionId}` - ); - } - - toString(): string { - return this.base; - } -} - -class TeamChannel extends ActionChannel { - constructor(teamId: string = '*') { - super(`teams.${teamId}`); - } -} - -class MembershipChannel extends ActionChannel { - constructor(membershipId: string = '*') { - super(`memberships.${membershipId}`); - } -} - -/** - * ---------------------------- - * Channel Type (for type checking) - * ---------------------------- - */ -export type ChannelValue = string | ResolvedChannel | DatabaseChannel | CollectionChannel | DocumentChannel | TablesDBChannel | TableChannel | RowChannel | BucketChannel | FileChannel | FunctionChannel | ExecutionChannel | TeamChannel | MembershipChannel; - -export class Channel { - /** - * Generate a database channel builder. - * - * @param {string} databaseId - * @returns {DatabaseChannel} - */ - static database(databaseId: string = '*'): DatabaseChannel { - return new DatabaseChannel(databaseId); - } - - /** - * Generate a tables database channel builder. - * - * @param {string} databaseId - * @returns {TablesDBChannel} - */ - static tablesdb(databaseId: string = '*'): TablesDBChannel { - return new TablesDBChannel(databaseId); - } - - /** - * Generate a buckets channel builder. - * - * @param {string} bucketId - * @returns {BucketChannel} - */ - static buckets(bucketId: string = '*'): BucketChannel { - return new BucketChannel(bucketId); - } - - /** - * Generate a functions channel builder. - * - * @param {string} functionId - * @returns {FunctionChannel} - */ - static functions(functionId: string = '*'): FunctionChannel { - return new FunctionChannel(functionId); - } - - /** - * Generate a teams channel builder. - * - * @param {string} teamId - * @returns {TeamChannel} - */ - static teams(teamId: string = '*'): TeamChannel { - return new TeamChannel(teamId); - } - - /** - * Generate a memberships channel builder. - * - * @param {string} membershipId - * @returns {MembershipChannel} - */ - static memberships(membershipId: string = '*'): MembershipChannel { - return new MembershipChannel(membershipId); - } - - /** - * Generate an account channel string. - * - * @param {string} userId - * @returns {string} - */ - static account(userId: string = '*'): string { - return `account.${userId}`; - } -} + return this.resolve("delete"); + } + } + + // Entry point + + export class Channel { + static database(id: string = "*"): IDatabase { + return ChannelBuilder.start(["databases", normalize(id)]); + } + + static tablesdb(id: string = "*"): ITablesDB { + return ChannelBuilder.start(["tablesdb", normalize(id)]); + } + + static buckets(id: string = "*"): IBucket { + return ChannelBuilder.start(["buckets", normalize(id)]); + } + + static functions(id: string = "*"): IFunction { + return ChannelBuilder.start(["functions", normalize(id)]); + } + + static teams(id: string = "*"): Actionable { + return ChannelBuilder.start(["teams", normalize(id)]); + } + + static memberships(id: string = "*"): Actionable { + return ChannelBuilder.start(["memberships", normalize(id)]); + } + + static account(userId: string = ""): string { + return userId.trim() + ? `account.${normalize(userId)}` + : "account"; + } + + // Global events + + static get documents(): string { + return "documents"; + } + + static get rows(): string { + return "rows"; + } + + static get files(): string { + return "files"; + } + + static get executions(): string { + return "executions"; + } + } diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 639507d297..e7e618553f 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -1,5 +1,5 @@ import { Models } from './models'; -import { ChannelValue } from './channel'; +import { Actionable, ResolvedChannel } from './channel'; /** * Payload type representing a key-value pair with string keys and any values. @@ -553,7 +553,7 @@ class Client { * @deprecated Use the Realtime service instead. * @see Realtime * - * @param {string|string[]|ChannelValue|ChannelValue[]} channels + * @param {string|string[]|Actionable|ResolvedChannel|(Actionable|ResolvedChannel)[]} channels * Channel to subscribe - pass a single channel as a string or Channel builder instance, or multiple with an array. * * Possible channels are: @@ -582,16 +582,20 @@ class Client { * @param {(payload: RealtimeMessage) => void} callback Is called on every realtime update. * @returns {() => void} Unsubscribes from events. */ - subscribe(channels: string | string[] | ChannelValue | ChannelValue[], callback: (payload: RealtimeResponseEvent) => void): () => void { + subscribe(channels: string | string[] | Actionable | ResolvedChannel | (Actionable | ResolvedChannel)[], callback: (payload: RealtimeResponseEvent) => void): () => void { const channelArray = Array.isArray(channels) ? channels : [channels]; // Convert Channel instances to strings const channelStrings = channelArray.map(ch => { if (typeof ch === 'string') { return ch; } - // Channel builder instances have toString() method - if (ch && typeof ch.toString === 'function') { - return ch.toString(); + // Check if it's an Actionable instance (has channel() method) + if (ch && typeof (ch as Actionable).channel === 'function') { + return (ch as Actionable).channel(); + } + // Check if it's a ResolvedChannel (has toString() method) + if (ch && typeof (ch as ResolvedChannel).toString === 'function') { + return (ch as ResolvedChannel).toString(); } return String(ch); }); diff --git a/templates/web/src/services/realtime.ts.twig b/templates/web/src/services/realtime.ts.twig index d03e93903a..6d99fd8f4a 100644 --- a/templates/web/src/services/realtime.ts.twig +++ b/templates/web/src/services/realtime.ts.twig @@ -1,5 +1,5 @@ import { {{ spec.title | caseUcfirst}}Exception, Client } from '../client'; -import { ChannelValue } from '../channel'; +import { Actionable, ResolvedChannel } from '../channel'; export type RealtimeSubscription = { close: () => Promise; @@ -242,16 +242,20 @@ export class Realtime { * Convert a channel value to a string * * @private - * @param {string | ChannelValue} channel - Channel value (string or Channel builder instance) + * @param {string | Actionable | ResolvedChannel} channel - Channel value (string or Channel builder instance) * @returns {string} Channel string representation */ - private channelToString(channel: string | ChannelValue): string { + private channelToString(channel: string | Actionable | ResolvedChannel): string { if (typeof channel === 'string') { return channel; } - // Channel builder instances have toString() method - if (channel && typeof channel.toString === 'function') { - return channel.toString(); + // Check if it's an Actionable instance (has channel() method) + if (channel && typeof (channel as Actionable).channel === 'function') { + return (channel as Actionable).channel(); + } + // Check if it's a ResolvedChannel (has toString() method) + if (channel && typeof (channel as ResolvedChannel).toString === 'function') { + return (channel as ResolvedChannel).toString(); } return String(channel); } @@ -259,53 +263,53 @@ export class Realtime { /** * Subscribe to a single channel * - * @param {string | ChannelValue} channel - Channel name to subscribe to (string or Channel builder instance) + * @param {string | Actionable | ResolvedChannel} channel - Channel name to subscribe to (string or Channel builder instance) * @param {Function} callback - Callback function to handle events * @returns {Promise} Subscription object with close method */ public async subscribe( - channel: ChannelValue, + channel: string | Actionable | ResolvedChannel, callback: (event: RealtimeResponseEvent) => void ): Promise; /** * Subscribe to multiple channels * - * @param {(string | ChannelValue)[]} channels - Array of channel names to subscribe to (strings or Channel builder instances) + * @param {(string | Actionable | ResolvedChannel)[]} channels - Array of channel names to subscribe to (strings or Channel builder instances) * @param {Function} callback - Callback function to handle events * @returns {Promise} Subscription object with close method */ public async subscribe( - channels: (string | ChannelValue)[], + channels: (string | Actionable | ResolvedChannel)[], callback: (event: RealtimeResponseEvent) => void ): Promise; /** * Subscribe to a single channel with typed payload * - * @param {string | ChannelValue} channel - Channel name to subscribe to (string or Channel builder instance) + * @param {string | Actionable | ResolvedChannel} channel - Channel name to subscribe to (string or Channel builder instance) * @param {Function} callback - Callback function to handle events with typed payload * @returns {Promise} Subscription object with close method */ public async subscribe( - channel: ChannelValue, + channel: string | Actionable | ResolvedChannel, callback: (event: RealtimeResponseEvent) => void ): Promise; /** * Subscribe to multiple channels with typed payload * - * @param {(string | ChannelValue)[]} channels - Array of channel names to subscribe to (strings or Channel builder instances) + * @param {(string | Actionable | ResolvedChannel)[]} channels - Array of channel names to subscribe to (strings or Channel builder instances) * @param {Function} callback - Callback function to handle events with typed payload * @returns {Promise} Subscription object with close method */ public async subscribe( - channels: (string | ChannelValue)[], + channels: (string | Actionable | ResolvedChannel)[], callback: (event: RealtimeResponseEvent) => void ): Promise; public async subscribe( - channelsOrChannel: ChannelValue | (string | ChannelValue)[], + channelsOrChannel: string | Actionable | ResolvedChannel | (string | Actionable | ResolvedChannel)[], callback: (event: RealtimeResponseEvent) => void ): Promise { const channelArray = Array.isArray(channelsOrChannel) From 61710eae11a84cb00fa548d8e7ed14e99eebf3c2 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 30 Dec 2025 19:06:36 +0530 Subject: [PATCH 08/26] Refactor channel and interface definitions for improved clarity and consistency --- templates/flutter/lib/channel.dart.twig | 28 +++++++++---------- .../flutter/lib/src/realtime_mixin.dart.twig | 1 + tests/Base.php | 2 +- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/templates/flutter/lib/channel.dart.twig b/templates/flutter/lib/channel.dart.twig index a80c18def5..afc81adf4f 100644 --- a/templates/flutter/lib/channel.dart.twig +++ b/templates/flutter/lib/channel.dart.twig @@ -1,7 +1,7 @@ -part of appwrite_channel; +part of {{ language.params.packageName }}; // The result of building the channel. -final class ResolvedChannel { +class ResolvedChannel { final String value; const ResolvedChannel(this.value); @@ -14,44 +14,44 @@ String _normalize(String id) => id.trim().isEmpty ? '*' : id; // State interfaces -abstract interface class Actionable { +abstract class Actionable { ResolvedChannel create(); ResolvedChannel update(); ResolvedChannel delete(); String get channel; } -abstract interface class IDatabase { +abstract class IDatabase { ICollection collection([String id = '*']); } -abstract interface class ICollection { +abstract class ICollection { IDocument document([String id = '*']); } -abstract interface class IDocument implements Actionable {} +abstract class IDocument implements Actionable {} -abstract interface class ITablesDB { +abstract class ITablesDB { ITable table([String id = '*']); } -abstract interface class ITable { +abstract class ITable { IRow row([String id = '*']); } -abstract interface class IRow implements Actionable {} +abstract class IRow implements Actionable {} -abstract interface class IBucket { +abstract class IBucket { IFile file([String id = '*']); } -abstract interface class IFile implements Actionable {} +abstract class IFile implements Actionable {} -abstract interface class IFunction { +abstract class IFunction { IExecution execution([String id = '*']); } -abstract interface class IExecution implements Actionable {} +abstract class IExecution implements Actionable {} // Builder implementation @@ -109,7 +109,7 @@ class _ChannelBuilder implements // Entry point -abstract final class Channel { +abstract class Channel { static IDatabase database([String id = '*']) => _ChannelBuilder._(['databases', _normalize(id)]); diff --git a/templates/flutter/lib/src/realtime_mixin.dart.twig b/templates/flutter/lib/src/realtime_mixin.dart.twig index f78de3bdb6..bc345cd0c2 100644 --- a/templates/flutter/lib/src/realtime_mixin.dart.twig +++ b/templates/flutter/lib/src/realtime_mixin.dart.twig @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/status.dart' as status; +import 'package:{{ language.params.packageName }}/{{ language.params.packageName }}.dart'; import 'exception.dart'; import 'realtime_subscription.dart'; import 'client.dart'; diff --git a/tests/Base.php b/tests/Base.php index 2a2b3e17e8..a82b5a3e97 100644 --- a/tests/Base.php +++ b/tests/Base.php @@ -170,7 +170,7 @@ abstract class Base extends TestCase 'tablesdb.*.tables.*.rows.*', 'tablesdb.db1.tables.table1.rows.row1', 'tablesdb.db1.tables.table1.rows.row1.update', - 'account.*', + 'account', 'account.user123', 'buckets.*.files.*', 'buckets.bucket1.files.file1', From 9252cc32214b9014e647d07d043efc9734cb58d9 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 30 Dec 2025 19:28:48 +0530 Subject: [PATCH 09/26] fixed flutter --- .../flutter/lib/src/realtime_mixin.dart.twig | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/templates/flutter/lib/src/realtime_mixin.dart.twig b/templates/flutter/lib/src/realtime_mixin.dart.twig index bc345cd0c2..7a1557be4a 100644 --- a/templates/flutter/lib/src/realtime_mixin.dart.twig +++ b/templates/flutter/lib/src/realtime_mixin.dart.twig @@ -3,7 +3,6 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/status.dart' as status; -import 'package:{{ language.params.packageName }}/{{ language.params.packageName }}.dart'; import 'exception.dart'; import 'realtime_subscription.dart'; import 'client.dart'; @@ -175,13 +174,18 @@ mixin RealtimeMixin { return channel; } // Check if it's an Actionable instance (has channel getter) - if (channel is Actionable) { - return channel.channel; - } - // Check if it's a ResolvedChannel (has toString() method) - if (channel is ResolvedChannel) { - return channel.toString(); + // Use dynamic to avoid import conflicts with part file types + final dynamic ch = channel; + try { + // Try to access the channel property (Actionable interface) + final channelValue = ch.channel; + if (channelValue is String) { + return channelValue; + } + } catch (_) { + // Not an Actionable, fall through to toString() } + // Check if it's a ResolvedChannel or any other type with toString() return channel.toString(); } From 4c3bef7bef61b259c65d2f2de1c0865c91fbfd41 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 30 Dec 2025 21:26:28 +0530 Subject: [PATCH 10/26] updated kotlin and swift --- .../src/main/java/io/package/Channel.kt.twig | 189 ++++++--------- templates/apple/Sources/Channel.swift.twig | 215 ++++++------------ 2 files changed, 133 insertions(+), 271 deletions(-) diff --git a/templates/android/library/src/main/java/io/package/Channel.kt.twig b/templates/android/library/src/main/java/io/package/Channel.kt.twig index 7235456fab..44639ba39f 100644 --- a/templates/android/library/src/main/java/io/package/Channel.kt.twig +++ b/templates/android/library/src/main/java/io/package/Channel.kt.twig @@ -21,173 +21,114 @@ interface Actionable { fun delete(): ResolvedChannel } -// Base actionable channel -abstract class ActionChannel(protected val base: String) : Actionable { - - override val channel: String - get() = base - - override fun create(): ResolvedChannel { - return ResolvedChannel("$base.create") - } - - override fun update(): ResolvedChannel { - return ResolvedChannel("$base.update") - } - - override fun delete(): ResolvedChannel { - return ResolvedChannel("$base.delete") - } - - override fun toString(): String { - return base - } +interface IDatabase { + fun collection(id: String = "*"): ICollection } -// Database → Collection → Document - -class DocumentChannel(base: String) : ActionChannel(base) - -class CollectionChannel( - databaseId: String, - collectionId: String -) { - private val base = - "databases.${normalize(databaseId)}.collections.${normalize(collectionId)}" +interface ICollection { + fun document(id: String = "*"): IDocument +} - fun document(documentId: String = "*"): DocumentChannel { - return DocumentChannel("$base.documents.${normalize(documentId)}") - } +interface IDocument : Actionable - override fun toString(): String { - return base - } +interface ITablesDB { + fun table(id: String = "*"): ITable } -class DatabaseChannel(databaseId: String) { - private val databaseId = normalize(databaseId) - private val base = "databases.$databaseId" +interface ITable { + fun row(id: String = "*"): IRow +} - fun collection(collectionId: String = "*"): CollectionChannel { - return CollectionChannel(databaseId, collectionId) - } +interface IRow : Actionable - override fun toString(): String { - return base - } +interface IBucket { + fun file(id: String = "*"): IFile } -// TablesDB → Table → Row +interface IFile : Actionable -class RowChannel(base: String) : ActionChannel(base) +interface IFunction { + fun execution(id: String = "*"): IExecution +} -class TableChannel( - databaseId: String, - tableId: String -) { - private val base = - "tablesdb.${normalize(databaseId)}.tables.${normalize(tableId)}" +interface IExecution : Actionable - fun row(rowId: String = "*"): RowChannel { - return RowChannel("$base.rows.${normalize(rowId)}") - } +private class ChannelBuilder( + private val segments: List +) : IDatabase, ICollection, IDocument, + ITablesDB, ITable, IRow, + IBucket, IFile, + IFunction, IExecution, Actionable { - override fun toString(): String { - return base + companion object { + fun start(segments: List) = ChannelBuilder(segments) } -} - -class TablesDBChannel(databaseId: String) { - private val databaseId = normalize(databaseId) - private val base = "tablesdb.$databaseId" - fun table(tableId: String = "*"): TableChannel { - return TableChannel(databaseId, tableId) + private fun next(segment: String, id: String): ChannelBuilder { + return ChannelBuilder(segments + listOf(segment, normalize(id))) } - override fun toString(): String { - return base - } -} + override fun collection(id: String): ICollection = + next("collections", id) -// Buckets → File + override fun document(id: String): IDocument = + next("documents", id) -class FileChannel(base: String) : ActionChannel(base) + override fun table(id: String): ITable = + next("tables", id) -class BucketChannel(bucketId: String) { - private val bucketId = normalize(bucketId) - private val base = "buckets.$bucketId" + override fun row(id: String): IRow = + next("rows", id) - fun file(fileId: String = "*"): FileChannel { - return FileChannel("$base.files.${normalize(fileId)}") - } + override fun file(id: String): IFile = + next("files", id) - override fun toString(): String { - return base - } -} + override fun execution(id: String): IExecution = + next("executions", id) -// Functions → Execution + override val channel: String + get() = segments.joinToString(".") -class ExecutionChannel(base: String) : ActionChannel(base) + override fun create(): ResolvedChannel = + ResolvedChannel("$channel.create") -class FunctionChannel(functionId: String) { - private val functionId = normalize(functionId) - private val base = "functions.$functionId" + override fun update(): ResolvedChannel = + ResolvedChannel("$channel.update") - fun execution(executionId: String = "*"): ExecutionChannel { - return ExecutionChannel("$base.executions.${normalize(executionId)}") - } + override fun delete(): ResolvedChannel = + ResolvedChannel("$channel.delete") - override fun toString(): String { - return base - } + override fun toString(): String = channel } -// Teams & Memberships - -class TeamChannel(teamId: String = "*") : - ActionChannel("teams.${normalize(teamId)}") +class Channel private constructor() { -class MembershipChannel(membershipId: String = "*") : - ActionChannel("memberships.${normalize(membershipId)}") - -// Entry point - -class Channel { companion object { - fun database(databaseId: String = "*"): DatabaseChannel { - return DatabaseChannel(databaseId) - } + fun database(id: String = "*"): IDatabase = + ChannelBuilder.start(listOf("databases", normalize(id))) - fun tablesdb(databaseId: String = "*"): TablesDBChannel { - return TablesDBChannel(databaseId) - } + fun tablesdb(id: String = "*"): ITablesDB = + ChannelBuilder.start(listOf("tablesdb", normalize(id))) - fun buckets(bucketId: String = "*"): BucketChannel { - return BucketChannel(bucketId) - } + fun buckets(id: String = "*"): IBucket = + ChannelBuilder.start(listOf("buckets", normalize(id))) - fun functions(functionId: String = "*"): FunctionChannel { - return FunctionChannel(functionId) - } + fun functions(id: String = "*"): IFunction = + ChannelBuilder.start(listOf("functions", normalize(id))) - fun teams(teamId: String = "*"): TeamChannel { - return TeamChannel(teamId) - } + fun teams(id: String = "*"): Actionable = + ChannelBuilder.start(listOf("teams", normalize(id))) - fun memberships(membershipId: String = "*"): MembershipChannel { - return MembershipChannel(membershipId) - } + fun memberships(id: String = "*"): Actionable = + ChannelBuilder.start(listOf("memberships", normalize(id))) fun account(userId: String = ""): String { - val normalized = normalize(userId) - return if (normalized == "*") "account" else "account.$normalized" + val id = normalize(userId) + return if (id == "*") "account" else "account.$id" } // Global events - const val documents = "documents" const val rows = "rows" const val files = "files" diff --git a/templates/apple/Sources/Channel.swift.twig b/templates/apple/Sources/Channel.swift.twig index 3ffb7b5205..13953294d0 100644 --- a/templates/apple/Sources/Channel.swift.twig +++ b/templates/apple/Sources/Channel.swift.twig @@ -27,212 +27,133 @@ public protocol Actionable { func delete() -> ResolvedChannel } -// Base actionable channel -open class ActionChannel: Actionable { - let base: String - - public var channel: String { - return base - } - - init(base: String) { - self.base = base - } - - public func create() -> ResolvedChannel { - ResolvedChannel(value: "\(base).create") - } - - public func update() -> ResolvedChannel { - ResolvedChannel(value: "\(base).update") - } - - public func delete() -> ResolvedChannel { - ResolvedChannel(value: "\(base).delete") - } - - public func toString() -> String { - base - } +public protocol IDatabase { + func collection(_ id: String) -> ICollection } -// Database → Collection → Document - -public final class DocumentChannel: ActionChannel {} - -public final class CollectionChannel { - private let base: String - private let databaseId: String - private let collectionId: String +public protocol ICollection { + func document(_ id: String) -> IDocument +} - init(databaseId: String, collectionId: String) { - self.databaseId = normalize(databaseId) - self.collectionId = normalize(collectionId) - self.base = "databases.\(self.databaseId).collections.\(self.collectionId)" - } +public protocol IDocument: Actionable {} - public func document(_ documentId: String = "*") -> DocumentChannel { - DocumentChannel(base: "\(base).documents.\(normalize(documentId))") - } +public protocol ITablesDB { + func table(_ id: String) -> ITable +} - public func toString() -> String { - base - } +public protocol ITable { + func row(_ id: String) -> IRow } -public final class DatabaseChannel { - private let base: String - private let databaseId: String +public protocol IRow: Actionable {} - init(databaseId: String) { - self.databaseId = normalize(databaseId) - self.base = "databases.\(self.databaseId)" - } +public protocol IBucket { + func file(_ id: String) -> IFile +} - public func collection(_ collectionId: String = "*") -> CollectionChannel { - CollectionChannel(databaseId: databaseId, collectionId: collectionId) - } +public protocol IFile: Actionable {} - public func toString() -> String { - base - } +public protocol IFunction { + func execution(_ id: String) -> IExecution } -// TablesDB → Table → Row - -public final class RowChannel: ActionChannel {} +public protocol IExecution: Actionable {} -public final class TableChannel { - private let base: String - private let databaseId: String - private let tableId: String +final class ChannelBuilder: + IDatabase, ICollection, IDocument, + ITablesDB, ITable, IRow, + IBucket, IFile, + IFunction, IExecution, Actionable +{ + private let segments: [String] - init(databaseId: String, tableId: String) { - self.databaseId = normalize(databaseId) - self.tableId = normalize(tableId) - self.base = "tablesdb.\(self.databaseId).tables.\(self.tableId)" + private init(_ segments: [String]) { + self.segments = segments } - public func row(_ rowId: String = "*") -> RowChannel { - RowChannel(base: "\(base).rows.\(normalize(rowId))") + static func start(_ segments: [String]) -> ChannelBuilder { + ChannelBuilder(segments) } - public func toString() -> String { - base + private func next(_ segment: String, _ id: String) -> ChannelBuilder { + ChannelBuilder(segments + [segment, normalize(id)]) } -} -public final class TablesDBChannel { - private let base: String - private let databaseId: String + // MARK: hierarchy - init(databaseId: String) { - self.databaseId = normalize(databaseId) - self.base = "tablesdb.\(self.databaseId)" + func collection(_ id: String = "*") -> ICollection { + next("collections", id) } - public func table(_ tableId: String = "*") -> TableChannel { - TableChannel(databaseId: databaseId, tableId: tableId) + func document(_ id: String = "*") -> IDocument { + next("documents", id) } - public func toString() -> String { - base + func table(_ id: String = "*") -> ITable { + next("tables", id) } -} - -// Buckets → File - -public final class FileChannel: ActionChannel {} -public final class BucketChannel { - private let base: String - private let bucketId: String - - init(bucketId: String) { - self.bucketId = normalize(bucketId) - self.base = "buckets.\(self.bucketId)" + func row(_ id: String = "*") -> IRow { + next("rows", id) } - public func file(_ fileId: String = "*") -> FileChannel { - FileChannel(base: "\(base).files.\(normalize(fileId))") + func file(_ id: String = "*") -> IFile { + next("files", id) } - public func toString() -> String { - base + func execution(_ id: String = "*") -> IExecution { + next("executions", id) } -} -// Functions → Execution - -public final class ExecutionChannel: ActionChannel {} - -public final class FunctionChannel { - private let base: String - private let functionId: String - - init(functionId: String) { - self.functionId = normalize(functionId) - self.base = "functions.\(self.functionId)" + var channel: String { + segments.joined(separator: ".") } - public func execution(_ executionId: String = "*") -> ExecutionChannel { - ExecutionChannel(base: "\(base).executions.\(normalize(executionId))") + func create() -> ResolvedChannel { + ResolvedChannel("\(channel).create") } - public func toString() -> String { - base + func update() -> ResolvedChannel { + ResolvedChannel("\(channel).update") } -} - -// Teams & Memberships -public final class TeamChannel: ActionChannel { - init(teamId: String = "*") { - super.init(base: "teams.\(normalize(teamId))") + func delete() -> ResolvedChannel { + ResolvedChannel("\(channel).delete") } } -public final class MembershipChannel: ActionChannel { - init(membershipId: String = "*") { - super.init(base: "memberships.\(normalize(membershipId))") - } -} - -// Entry point +public enum Channel { -public final class Channel { - - public static func database(_ databaseId: String = "*") -> DatabaseChannel { - DatabaseChannel(databaseId: databaseId) + public static func database(_ id: String = "*") -> IDatabase { + ChannelBuilder.start(["databases", normalize(id)]) } - public static func tablesdb(_ databaseId: String = "*") -> TablesDBChannel { - TablesDBChannel(databaseId: databaseId) + public static func tablesdb(_ id: String = "*") -> ITablesDB { + ChannelBuilder.start(["tablesdb", normalize(id)]) } - public static func buckets(_ bucketId: String = "*") -> BucketChannel { - BucketChannel(bucketId: bucketId) + public static func buckets(_ id: String = "*") -> IBucket { + ChannelBuilder.start(["buckets", normalize(id)]) } - public static func functions(_ functionId: String = "*") -> FunctionChannel { - FunctionChannel(functionId: functionId) + public static func functions(_ id: String = "*") -> IFunction { + ChannelBuilder.start(["functions", normalize(id)]) } - public static func teams(_ teamId: String = "*") -> TeamChannel { - TeamChannel(teamId: teamId) + public static func teams(_ id: String = "*") -> Actionable { + ChannelBuilder.start(["teams", normalize(id)]) } - public static func memberships(_ membershipId: String = "*") -> MembershipChannel { - MembershipChannel(membershipId: membershipId) + public static func memberships(_ id: String = "*") -> Actionable { + ChannelBuilder.start(["memberships", normalize(id)]) } public static func account(_ userId: String = "") -> String { - let normalized = normalize(userId) - return normalized == "*" ? "account" : "account.\(normalized)" + let id = normalize(userId) + return id == "*" ? "account" : "account.\(id)" } // Global events - public static let documents = "documents" public static let rows = "rows" public static let files = "files" From 05046c81de2918492c7609adb744fc64026f2875 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 30 Dec 2025 22:06:32 +0530 Subject: [PATCH 11/26] fixed swift --- templates/apple/Sources/Channel.swift.twig | 65 +++++++++- .../Sources/Services/Realtime.swift.twig | 32 +---- tests/AppleSwift56Test.php | 120 +++++++++++++++++- 3 files changed, 182 insertions(+), 35 deletions(-) diff --git a/templates/apple/Sources/Channel.swift.twig b/templates/apple/Sources/Channel.swift.twig index 13953294d0..e2c464c9de 100644 --- a/templates/apple/Sources/Channel.swift.twig +++ b/templates/apple/Sources/Channel.swift.twig @@ -1,5 +1,19 @@ import Foundation +/** + * Protocol for channel values that can be converted to strings + */ +public protocol ChannelValue { + func toString() -> String +} + +// Make String conform to ChannelValue +extension String: ChannelValue { + public func toString() -> String { + return self + } +} + // The result of building the channel. public final class ResolvedChannel { private let value: String @@ -20,7 +34,7 @@ private func normalize(_ id: String) -> String { } // Protocol for actionable channels -public protocol Actionable { +public protocol Actionable: ChannelValue { var channel: String { get } func create() -> ResolvedChannel func update() -> ResolvedChannel @@ -59,6 +73,45 @@ public protocol IFunction { public protocol IExecution: Actionable {} +// Protocol extensions to provide default parameter overloads +// Swift doesn't allow default parameters in protocol requirements, +// so we provide overloaded methods without parameters +extension IDatabase { + public func collection() -> ICollection { + return collection("*") + } +} + +extension ICollection { + public func document() -> IDocument { + return document("*") + } +} + +extension ITablesDB { + public func table() -> ITable { + return table("*") + } +} + +extension ITable { + public func row() -> IRow { + return row("*") + } +} + +extension IBucket { + public func file() -> IFile { + return file("*") + } +} + +extension IFunction { + public func execution() -> IExecution { + return execution("*") + } +} + final class ChannelBuilder: IDatabase, ICollection, IDocument, ITablesDB, ITable, IRow, @@ -109,16 +162,20 @@ final class ChannelBuilder: segments.joined(separator: ".") } + func toString() -> String { + return channel + } + func create() -> ResolvedChannel { - ResolvedChannel("\(channel).create") + ResolvedChannel(value: "\(channel).create") } func update() -> ResolvedChannel { - ResolvedChannel("\(channel).update") + ResolvedChannel(value: "\(channel).update") } func delete() -> ResolvedChannel { - ResolvedChannel("\(channel).delete") + ResolvedChannel(value: "\(channel).delete") } } diff --git a/templates/apple/Sources/Services/Realtime.swift.twig b/templates/apple/Sources/Services/Realtime.swift.twig index c880b226a1..a588e7675f 100644 --- a/templates/apple/Sources/Services/Realtime.swift.twig +++ b/templates/apple/Sources/Services/Realtime.swift.twig @@ -3,22 +3,6 @@ import AsyncHTTPClient import NIO import NIOHTTP1 -/** - * Protocol for channel values that can be converted to strings - */ -public protocol ChannelValue { - func toString() -> String -} - -extension String: ChannelValue { - public func toString() -> String { - return self - } -} - -extension ResolvedChannel: ChannelValue {} -extension ActionChannel: ChannelValue {} - open class Realtime : Service { private let TYPE_ERROR = "error" @@ -135,20 +119,8 @@ open class Realtime : Service { /** * Convert channel value to string - * Checks if it's an Actionable instance (use .channel), - * then ResolvedChannel (use .toString()), - * otherwise treats as string */ private func channelToString(_ channel: ChannelValue) -> String { - // Check if it's an Actionable instance (has channel property) - if let actionable = channel as? Actionable { - return actionable.channel - } - // Check if it's a ResolvedChannel (has toString() method) - if let resolved = channel as? ResolvedChannel { - return resolved.toString() - } - // Otherwise use toString() (works for strings and other channel types) return channel.toString() } @@ -252,7 +224,7 @@ open class Realtime : Service { extension Realtime: WebSocketClientDelegate { - public func onOpen(channel: Channel) { + public func onOpen(channel: NIO.Channel) { self.reconnectAttempts = 0 onOpenCallbacks.forEach { $0() } startHeartbeat() @@ -272,7 +244,7 @@ extension Realtime: WebSocketClientDelegate { } } - public func onClose(channel: Channel, data: Data) async throws { + public func onClose(channel: NIO.Channel, data: Data) async throws { stopHeartbeat() onCloseCallbacks.forEach { $0() } diff --git a/tests/AppleSwift56Test.php b/tests/AppleSwift56Test.php index 36852cae07..ddd482f604 100644 --- a/tests/AppleSwift56Test.php +++ b/tests/AppleSwift56Test.php @@ -16,7 +16,7 @@ class AppleSwift56Test extends Base 'cp tests/languages/apple/Tests.swift tests/sdks/apple/Tests/AppwriteTests/Tests.swift', ]; protected string $command = - 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/apple swift:5.6-focal swift test'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/apple swift:5.6-focal swift test 2>&1'; protected array $expectedOutput = [ ...Base::PING_RESPONSE, @@ -34,4 +34,122 @@ class AppleSwift56Test extends Base ...Base::CHANNEL_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES ]; + + /** + * Override to log all output including compilation errors + */ + public function testHTTPSuccess(): void + { + $spec = file_get_contents(realpath(__DIR__ . '/resources/spec.json')); + + if (empty($spec)) { + throw new \Exception('Failed to parse spec.'); + } + + $sdk = new \Appwrite\SDK\SDK($this->getLanguage(), new \Appwrite\Spec\Swagger2($spec)); + + $sdk + ->setName($this->sdkName) + ->setVersion($this->version) + ->setPlatform($this->sdkPlatform) + ->setDescription('Repo description goes here') + ->setShortDescription('Repo short description goes here') + ->setLogo('https://appwrite.io/v1/images/console.png') + ->setWarning('**WORK IN PROGRESS - THIS IS JUST A TEST SDK**') + ->setExamples('**EXAMPLES** ') + ->setNamespace("io appwrite") + ->setGitUserName('repoowner') + ->setGitRepoName('reponame') + ->setLicense('BSD-3-Clause') + ->setLicenseContent('demo license') + ->setChangelog('--changelog--') + ->setDefaultHeaders([ + 'X-Appwrite-Response-Format' => '0.8.0', + ]) + ->setTest("true"); + + $dir = __DIR__ . '/sdks/' . $this->language; + + $this->rmdirRecursive($dir); + + $sdk->generate(__DIR__ . '/sdks/' . $this->language); + + /** + * Build SDK + */ + foreach ($this->build as $command) { + echo "Build Executing: {$command}\n"; + exec($command); + } + + $output = []; + $returnVar = 0; + + echo "Env Executing: {$this->command}\n"; + + // Capture both stdout and stderr, and return code + exec($this->command, $output, $returnVar); + + // Log ALL output including compilation errors + echo "\n=== FULL OUTPUT (including errors) ===\n"; + echo implode("\n", $output); + echo "\n=== END FULL OUTPUT ===\n"; + echo "Return code: {$returnVar}\n\n"; + + $this->assertIsArray($output); + + // Save original output for error logging + $originalOutput = $output; + + do { + $removed = \array_shift($output); + } while ($removed != 'Test Started' && sizeof($output) != 0); + + // If we filtered everything out, it means "Test Started" was never found + // This likely means there were compilation errors + if (sizeof($output) == 0 && sizeof($originalOutput) > 0) { + echo "\n=== ERROR: 'Test Started' marker not found. Full output above shows compilation errors. ===\n"; + } + + echo \implode("\n", $output); + + foreach ($this->expectedOutput as $index => $expected) { + if (!isset($output[$index])) { + echo "\n=== ERROR: Missing output at index {$index}. Expected: {$expected} ===\n"; + continue; + } + // HACK: Swift does not guarantee the order of the JSON parameters + if (\str_starts_with($expected, '{')) { + $this->assertEquals( + \json_decode($expected, true), + \json_decode($output[$index], true) + ); + } elseif ($expected == 'unique()') { + $this->assertNotEmpty($output[$index]); + $this->assertIsString($output[$index]); + $this->assertEquals(20, strlen($output[$index])); + $this->assertNotEquals($output[$index], 'unique()'); + } else { + $this->assertEquals($expected, $output[$index]); + } + } + } + + private function rmdirRecursive($dir): void + { + if (!\is_dir($dir)) { + return; + } + foreach (\scandir($dir) as $file) { + if ('.' === $file || '..' === $file) { + continue; + } + if (\is_dir("$dir/$file")) { + $this->rmdirRecursive("$dir/$file"); + } else { + \unlink("$dir/$file"); + } + } + \rmdir($dir); + } } From 9f5f63587a1f7d80c2f79f6f233dc9b3c6052d0f Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 9 Jan 2026 14:01:57 +0530 Subject: [PATCH 12/26] Refactor channel method names for consistency across languages --- .../src/main/java/io/package/Channel.kt.twig | 70 ++++++++++--------- templates/apple/Sources/Channel.swift.twig | 6 +- templates/dart/test/channel_test.dart.twig | 20 +++--- templates/flutter/lib/channel.dart.twig | 6 +- .../flutter/test/src/channel_test.dart.twig | 18 ++--- templates/web/src/channel.ts.twig | 6 +- templates/web/src/client.ts.twig | 6 +- tests/languages/android/Tests.kt | 18 ++--- tests/languages/apple/Tests.swift | 18 ++--- tests/languages/flutter/tests.dart | 18 ++--- tests/languages/web/index.html | 18 ++--- tests/languages/web/node.js | 18 ++--- 12 files changed, 114 insertions(+), 108 deletions(-) diff --git a/templates/android/library/src/main/java/io/package/Channel.kt.twig b/templates/android/library/src/main/java/io/package/Channel.kt.twig index 44639ba39f..551cbeb826 100644 --- a/templates/android/library/src/main/java/io/package/Channel.kt.twig +++ b/templates/android/library/src/main/java/io/package/Channel.kt.twig @@ -1,19 +1,25 @@ package {{ sdk.namespace | caseDot }} -// The result of building the channel. +/** + * The result of building the channel. + */ class ResolvedChannel(private val value: String) { override fun toString(): String { return value } } -// Helper for normalizing IDs +/** + * Helper for normalizing IDs + */ private fun normalize(id: String): String { val trimmed = id.trim() return if (trimmed.isEmpty()) "*" else trimmed } -// Interface for actionable channels +/** + * Interface for actionable channels + */ interface Actionable { val channel: String fun create(): ResolvedChannel @@ -21,44 +27,44 @@ interface Actionable { fun delete(): ResolvedChannel } -interface IDatabase { - fun collection(id: String = "*"): ICollection +interface Database { + fun collection(id: String = "*"): Collection } -interface ICollection { +interface Collection { fun document(id: String = "*"): IDocument } interface IDocument : Actionable -interface ITablesDB { - fun table(id: String = "*"): ITable +interface TablesDB { + fun table(id: String = "*"): Table } -interface ITable { - fun row(id: String = "*"): IRow +interface Table { + fun row(id: String = "*"): Row } -interface IRow : Actionable +interface Row : Actionable -interface IBucket { - fun file(id: String = "*"): IFile +interface Bucket { + fun file(id: String = "*"): File } -interface IFile : Actionable +interface File : Actionable -interface IFunction { - fun execution(id: String = "*"): IExecution +interface Function { + fun execution(id: String = "*"): Execution } -interface IExecution : Actionable +interface Execution : Actionable private class ChannelBuilder( private val segments: List -) : IDatabase, ICollection, IDocument, - ITablesDB, ITable, IRow, - IBucket, IFile, - IFunction, IExecution, Actionable { +) : Database, Collection, IDocument, + TablesDB, Table, Row, + Bucket, File, + Function, Execution, Actionable { companion object { fun start(segments: List) = ChannelBuilder(segments) @@ -68,22 +74,22 @@ private class ChannelBuilder( return ChannelBuilder(segments + listOf(segment, normalize(id))) } - override fun collection(id: String): ICollection = + override fun collection(id: String): Collection = next("collections", id) override fun document(id: String): IDocument = next("documents", id) - override fun table(id: String): ITable = + override fun table(id: String): Table = next("tables", id) - override fun row(id: String): IRow = + override fun row(id: String): Row = next("rows", id) - override fun file(id: String): IFile = + override fun file(id: String): File = next("files", id) - override fun execution(id: String): IExecution = + override fun execution(id: String): Execution = next("executions", id) override val channel: String @@ -105,22 +111,22 @@ class Channel private constructor() { companion object { - fun database(id: String = "*"): IDatabase = + fun database(id: String = "*"): Database = ChannelBuilder.start(listOf("databases", normalize(id))) - fun tablesdb(id: String = "*"): ITablesDB = + fun tablesdb(id: String = "*"): TablesDB = ChannelBuilder.start(listOf("tablesdb", normalize(id))) - fun buckets(id: String = "*"): IBucket = + fun buckets(id: String = "*"): Bucket = ChannelBuilder.start(listOf("buckets", normalize(id))) - fun functions(id: String = "*"): IFunction = + fun function(id: String = "*"): Function = ChannelBuilder.start(listOf("functions", normalize(id))) - fun teams(id: String = "*"): Actionable = + fun team(id: String = "*"): Actionable = ChannelBuilder.start(listOf("teams", normalize(id))) - fun memberships(id: String = "*"): Actionable = + fun membership(id: String = "*"): Actionable = ChannelBuilder.start(listOf("memberships", normalize(id))) fun account(userId: String = ""): String { diff --git a/templates/apple/Sources/Channel.swift.twig b/templates/apple/Sources/Channel.swift.twig index e2c464c9de..516add320c 100644 --- a/templates/apple/Sources/Channel.swift.twig +++ b/templates/apple/Sources/Channel.swift.twig @@ -193,15 +193,15 @@ public enum Channel { ChannelBuilder.start(["buckets", normalize(id)]) } - public static func functions(_ id: String = "*") -> IFunction { + public static func function(_ id: String = "*") -> IFunction { ChannelBuilder.start(["functions", normalize(id)]) } - public static func teams(_ id: String = "*") -> Actionable { + public static func team(_ id: String = "*") -> Actionable { ChannelBuilder.start(["teams", normalize(id)]) } - public static func memberships(_ id: String = "*") -> Actionable { + public static func membership(_ id: String = "*") -> Actionable { ChannelBuilder.start(["memberships", normalize(id)]) } diff --git a/templates/dart/test/channel_test.dart.twig b/templates/dart/test/channel_test.dart.twig index 1116d1ac73..ed0856e6b6 100644 --- a/templates/dart/test/channel_test.dart.twig +++ b/templates/dart/test/channel_test.dart.twig @@ -84,56 +84,56 @@ void main() { group('functions()', () { test('returns functions channel builder', () { - final builder = Channel.functions(); + final builder = Channel.function(); expect(builder.channel, 'functions.*'); }); test('returns functions channel with specific ID', () { - final builder = Channel.functions('func1'); + final builder = Channel.function('func1'); expect(builder.channel, 'functions.func1'); }); test('returns functions channel with execution', () { - final channel = Channel.functions('func1').execution('exec1'); + final channel = Channel.function('func1').execution('exec1'); expect(channel.channel, 'functions.func1.executions.exec1'); }); test('returns functions channel with action', () { - final channel = Channel.functions('func1').execution('exec1').create(); + final channel = Channel.function('func1').execution('exec1').create(); expect(channel.toString(), 'functions.func1.executions.exec1.create'); }); }); group('teams()', () { test('returns teams channel builder', () { - final builder = Channel.teams(); + final builder = Channel.team(); expect(builder.channel, 'teams.*'); }); test('returns teams channel with specific team ID', () { - final builder = Channel.teams('team1'); + final builder = Channel.team('team1'); expect(builder.channel, 'teams.team1'); }); test('returns teams channel with action', () { - final channel = Channel.teams('team1').create(); + final channel = Channel.team('team1').create(); expect(channel.toString(), 'teams.team1.create'); }); }); group('memberships()', () { test('returns memberships channel builder', () { - final builder = Channel.memberships(); + final builder = Channel.membership(); expect(builder.channel, 'memberships.*'); }); test('returns memberships channel with specific membership ID', () { - final builder = Channel.memberships('membership1'); + final builder = Channel.membership('membership1'); expect(builder.channel, 'memberships.membership1'); }); test('returns memberships channel with action', () { - final channel = Channel.memberships('membership1').update(); + final channel = Channel.membership('membership1').update(); expect(channel.toString(), 'memberships.membership1.update'); }); }); diff --git a/templates/flutter/lib/channel.dart.twig b/templates/flutter/lib/channel.dart.twig index afc81adf4f..0d38f120e9 100644 --- a/templates/flutter/lib/channel.dart.twig +++ b/templates/flutter/lib/channel.dart.twig @@ -119,13 +119,13 @@ abstract class Channel { static IBucket buckets([String id = '*']) => _ChannelBuilder._(['buckets', _normalize(id)]); - static IFunction functions([String id = '*']) => + static IFunction function([String id = '*']) => _ChannelBuilder._(['functions', _normalize(id)]); - static Actionable teams([String id = '*']) => + static Actionable team([String id = '*']) => _ChannelBuilder._(['teams', _normalize(id)]); - static Actionable memberships([String id = '*']) => + static Actionable membership([String id = '*']) => _ChannelBuilder._(['memberships', _normalize(id)]); static String account([String userId = '']) => diff --git a/templates/flutter/test/src/channel_test.dart.twig b/templates/flutter/test/src/channel_test.dart.twig index f531049ec0..0ff2b2a9e4 100644 --- a/templates/flutter/test/src/channel_test.dart.twig +++ b/templates/flutter/test/src/channel_test.dart.twig @@ -62,45 +62,45 @@ void main() { group('functions()', () { test('returns functions channel with defaults', () { - expect(Channel.functions().execution().toString(), 'functions.*.executions.*'); + expect(Channel.function().execution().toString(), 'functions.*.executions.*'); }); test('returns functions channel with specific IDs', () { - expect(Channel.functions('func1').execution('exec1').toString(), + expect(Channel.function('func1').execution('exec1').toString(), 'functions.func1.executions.exec1'); }); test('returns functions channel with action', () { - expect(Channel.functions('func1').execution('exec1').create().toString(), + expect(Channel.function('func1').execution('exec1').create().toString(), 'functions.func1.executions.exec1.create'); }); }); group('teams()', () { test('returns teams channel with default', () { - expect(Channel.teams().toString(), 'teams.*'); + expect(Channel.team().toString(), 'teams.*'); }); test('returns teams channel with specific team ID', () { - expect(Channel.teams('team1').toString(), 'teams.team1'); + expect(Channel.team('team1').toString(), 'teams.team1'); }); test('returns teams channel with action', () { - expect(Channel.teams('team1').create().toString(), 'teams.team1.create'); + expect(Channel.team('team1').create().toString(), 'teams.team1.create'); }); }); group('memberships()', () { test('returns memberships channel with default', () { - expect(Channel.memberships().toString(), 'memberships.*'); + expect(Channel.membership().toString(), 'memberships.*'); }); test('returns memberships channel with specific membership ID', () { - expect(Channel.memberships('membership1').toString(), 'memberships.membership1'); + expect(Channel.membership('membership1').toString(), 'memberships.membership1'); }); test('returns memberships channel with action', () { - expect(Channel.memberships('membership1').update().toString(), + expect(Channel.membership('membership1').update().toString(), 'memberships.membership1.update'); }); }); diff --git a/templates/web/src/channel.ts.twig b/templates/web/src/channel.ts.twig index f4fefdeb7a..8446bfda17 100644 --- a/templates/web/src/channel.ts.twig +++ b/templates/web/src/channel.ts.twig @@ -157,15 +157,15 @@ export class ResolvedChannel { return ChannelBuilder.start(["buckets", normalize(id)]); } - static functions(id: string = "*"): IFunction { + static function(id: string = "*"): IFunction { return ChannelBuilder.start(["functions", normalize(id)]); } - static teams(id: string = "*"): Actionable { + static team(id: string = "*"): Actionable { return ChannelBuilder.start(["teams", normalize(id)]); } - static memberships(id: string = "*"): Actionable { + static membership(id: string = "*"): Actionable { return ChannelBuilder.start(["memberships", normalize(id)]); } diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index e7e618553f..a2011f8634 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -576,9 +576,9 @@ class Client { * You can also use Channel builders: * - Channel.database('db').collection('col').document('doc').create() * - Channel.buckets('bucket').file('file').update() - * - Channel.functions('func').execution('exec').delete() - * - Channel.teams('team').create() - * - Channel.memberships('membership').update() + * - Channel.function('func').execution('exec').delete() + * - Channel.team('team').create() + * - Channel.membership('membership').update() * @param {(payload: RealtimeMessage) => void} callback Is called on every realtime update. * @returns {() => void} Unsubscribes from events. */ diff --git a/tests/languages/android/Tests.kt b/tests/languages/android/Tests.kt index 58013f6473..5f775cf064 100644 --- a/tests/languages/android/Tests.kt +++ b/tests/languages/android/Tests.kt @@ -282,15 +282,15 @@ class ServiceTest { writeToFile(Channel.buckets().file().toString()) writeToFile(Channel.buckets("bucket1").file("file1").toString()) writeToFile(Channel.buckets("bucket1").file("file1").delete().toString()) - writeToFile(Channel.functions().execution().toString()) - writeToFile(Channel.functions("func1").execution("exec1").toString()) - writeToFile(Channel.functions("func1").execution("exec1").create().toString()) - writeToFile(Channel.teams().toString()) - writeToFile(Channel.teams("team1").toString()) - writeToFile(Channel.teams("team1").create().toString()) - writeToFile(Channel.memberships().toString()) - writeToFile(Channel.memberships("membership1").toString()) - writeToFile(Channel.memberships("membership1").update().toString()) + writeToFile(Channel.function().execution().toString()) + writeToFile(Channel.function("func1").execution("exec1").toString()) + writeToFile(Channel.function("func1").execution("exec1").create().toString()) + writeToFile(Channel.team().toString()) + writeToFile(Channel.team("team1").toString()) + writeToFile(Channel.team("team1").create().toString()) + writeToFile(Channel.membership().toString()) + writeToFile(Channel.membership("membership1").toString()) + writeToFile(Channel.membership("membership1").update().toString()) // Operator helper tests writeToFile(Operator.increment(1)) diff --git a/tests/languages/apple/Tests.swift b/tests/languages/apple/Tests.swift index 50bde946c9..4b1324dce8 100644 --- a/tests/languages/apple/Tests.swift +++ b/tests/languages/apple/Tests.swift @@ -260,15 +260,15 @@ class Tests: XCTestCase { print(Channel.buckets().file().toString()) print(Channel.buckets("bucket1").file("file1").toString()) print(Channel.buckets("bucket1").file("file1").delete().toString()) - print(Channel.functions().execution().toString()) - print(Channel.functions("func1").execution("exec1").toString()) - print(Channel.functions("func1").execution("exec1").create().toString()) - print(Channel.teams().toString()) - print(Channel.teams("team1").toString()) - print(Channel.teams("team1").create().toString()) - print(Channel.memberships().toString()) - print(Channel.memberships("membership1").toString()) - print(Channel.memberships("membership1").update().toString()) + print(Channel.function().execution().toString()) + print(Channel.function("func1").execution("exec1").toString()) + print(Channel.function("func1").execution("exec1").create().toString()) + print(Channel.team().toString()) + print(Channel.team("team1").toString()) + print(Channel.team("team1").create().toString()) + print(Channel.membership().toString()) + print(Channel.membership("membership1").toString()) + print(Channel.membership("membership1").update().toString()) // Operator helper tests print(Operator.increment(1)) diff --git a/tests/languages/flutter/tests.dart b/tests/languages/flutter/tests.dart index 7647041c66..6592176e67 100644 --- a/tests/languages/flutter/tests.dart +++ b/tests/languages/flutter/tests.dart @@ -259,15 +259,15 @@ void main() async { print(Channel.buckets().file().toString()); print(Channel.buckets('bucket1').file('file1').toString()); print(Channel.buckets('bucket1').file('file1').delete().toString()); - print(Channel.functions().execution().toString()); - print(Channel.functions('func1').execution('exec1').toString()); - print(Channel.functions('func1').execution('exec1').create().toString()); - print(Channel.teams().toString()); - print(Channel.teams('team1').toString()); - print(Channel.teams('team1').create().toString()); - print(Channel.memberships().toString()); - print(Channel.memberships('membership1').toString()); - print(Channel.memberships('membership1').update().toString()); + print(Channel.function().execution().toString()); + print(Channel.function('func1').execution('exec1').toString()); + print(Channel.function('func1').execution('exec1').create().toString()); + print(Channel.team().toString()); + print(Channel.team('team1').toString()); + print(Channel.team('team1').create().toString()); + print(Channel.membership().toString()); + print(Channel.membership('membership1').toString()); + print(Channel.membership('membership1').update().toString()); // Operator helper tests print(Operator.increment(1)); diff --git a/tests/languages/web/index.html b/tests/languages/web/index.html index e8b4d48fe1..d7430a95b5 100644 --- a/tests/languages/web/index.html +++ b/tests/languages/web/index.html @@ -331,15 +331,15 @@ console.log(Channel.buckets().file().toString()); console.log(Channel.buckets('bucket1').file('file1').toString()); console.log(Channel.buckets('bucket1').file('file1').delete().toString()); - console.log(Channel.functions().execution().toString()); - console.log(Channel.functions('func1').execution('exec1').toString()); - console.log(Channel.functions('func1').execution('exec1').create().toString()); - console.log(Channel.teams().toString()); - console.log(Channel.teams('team1').toString()); - console.log(Channel.teams('team1').create().toString()); - console.log(Channel.memberships().toString()); - console.log(Channel.memberships('membership1').toString()); - console.log(Channel.memberships('membership1').update().toString()); + console.log(Channel.function().execution().toString()); + console.log(Channel.function('func1').execution('exec1').toString()); + console.log(Channel.function('func1').execution('exec1').create().toString()); + console.log(Channel.team().toString()); + console.log(Channel.team('team1').toString()); + console.log(Channel.team('team1').create().toString()); + console.log(Channel.membership().toString()); + console.log(Channel.membership('membership1').toString()); + console.log(Channel.membership('membership1').update().toString()); // Operator helper tests console.log(Operator.increment(1)); diff --git a/tests/languages/web/node.js b/tests/languages/web/node.js index 8e6ebd824e..6ee93e0f98 100644 --- a/tests/languages/web/node.js +++ b/tests/languages/web/node.js @@ -262,15 +262,15 @@ async function start() { console.log(Channel.buckets().file().toString()); console.log(Channel.buckets('bucket1').file('file1').toString()); console.log(Channel.buckets('bucket1').file('file1').delete().toString()); - console.log(Channel.functions().execution().toString()); - console.log(Channel.functions('func1').execution('exec1').toString()); - console.log(Channel.functions('func1').execution('exec1').create().toString()); - console.log(Channel.teams().toString()); - console.log(Channel.teams('team1').toString()); - console.log(Channel.teams('team1').create().toString()); - console.log(Channel.memberships().toString()); - console.log(Channel.memberships('membership1').toString()); - console.log(Channel.memberships('membership1').update().toString()); + console.log(Channel.function().execution().toString()); + console.log(Channel.function('func1').execution('exec1').toString()); + console.log(Channel.function('func1').execution('exec1').create().toString()); + console.log(Channel.team().toString()); + console.log(Channel.team('team1').toString()); + console.log(Channel.team('team1').create().toString()); + console.log(Channel.membership().toString()); + console.log(Channel.membership('membership1').toString()); + console.log(Channel.membership('membership1').update().toString()); // Operator helper tests console.log(Operator.increment(1)); From 0b840f9c8f2d4a84dedb55b8f84f0259bd701c29 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 9 Jan 2026 14:17:19 +0530 Subject: [PATCH 13/26] updated bucket --- .../library/src/main/java/io/package/Channel.kt.twig | 2 +- templates/apple/Sources/Channel.swift.twig | 2 +- templates/dart/test/channel_test.dart.twig | 10 +++++----- templates/flutter/lib/channel.dart.twig | 2 +- templates/flutter/lib/src/realtime.dart.twig | 2 +- templates/flutter/test/src/channel_test.dart.twig | 8 ++++---- templates/react-native/src/channel.ts.twig | 2 +- templates/web/src/channel.ts.twig | 2 +- templates/web/src/client.ts.twig | 2 +- tests/languages/android/Tests.kt | 6 +++--- tests/languages/apple/Tests.swift | 6 +++--- tests/languages/flutter/tests.dart | 6 +++--- tests/languages/web/index.html | 6 +++--- tests/languages/web/node.js | 6 +++--- 14 files changed, 31 insertions(+), 31 deletions(-) diff --git a/templates/android/library/src/main/java/io/package/Channel.kt.twig b/templates/android/library/src/main/java/io/package/Channel.kt.twig index 551cbeb826..61d7ea7842 100644 --- a/templates/android/library/src/main/java/io/package/Channel.kt.twig +++ b/templates/android/library/src/main/java/io/package/Channel.kt.twig @@ -117,7 +117,7 @@ class Channel private constructor() { fun tablesdb(id: String = "*"): TablesDB = ChannelBuilder.start(listOf("tablesdb", normalize(id))) - fun buckets(id: String = "*"): Bucket = + fun bucket(id: String = "*"): Bucket = ChannelBuilder.start(listOf("buckets", normalize(id))) fun function(id: String = "*"): Function = diff --git a/templates/apple/Sources/Channel.swift.twig b/templates/apple/Sources/Channel.swift.twig index 516add320c..cfd5a8773d 100644 --- a/templates/apple/Sources/Channel.swift.twig +++ b/templates/apple/Sources/Channel.swift.twig @@ -189,7 +189,7 @@ public enum Channel { ChannelBuilder.start(["tablesdb", normalize(id)]) } - public static func buckets(_ id: String = "*") -> IBucket { + public static func bucket(_ id: String = "*") -> IBucket { ChannelBuilder.start(["buckets", normalize(id)]) } diff --git a/templates/dart/test/channel_test.dart.twig b/templates/dart/test/channel_test.dart.twig index ed0856e6b6..0e67e33923 100644 --- a/templates/dart/test/channel_test.dart.twig +++ b/templates/dart/test/channel_test.dart.twig @@ -60,24 +60,24 @@ void main() { }); }); - group('buckets()', () { + group('bucket()', () { test('returns buckets channel builder', () { - final builder = Channel.buckets(); + final builder = Channel.bucket(); expect(builder.channel, 'buckets.*'); }); test('returns buckets channel with specific ID', () { - final builder = Channel.buckets('bucket1'); + final builder = Channel.bucket('bucket1'); expect(builder.channel, 'buckets.bucket1'); }); test('returns buckets channel with file', () { - final channel = Channel.buckets('bucket1').file('file1'); + final channel = Channel.bucket('bucket1').file('file1'); expect(channel.channel, 'buckets.bucket1.files.file1'); }); test('returns buckets channel with action', () { - final channel = Channel.buckets('bucket1').file('file1').delete(); + final channel = Channel.bucket('bucket1').file('file1').delete(); expect(channel.toString(), 'buckets.bucket1.files.file1.delete'); }); }); diff --git a/templates/flutter/lib/channel.dart.twig b/templates/flutter/lib/channel.dart.twig index 0d38f120e9..077f89d9ff 100644 --- a/templates/flutter/lib/channel.dart.twig +++ b/templates/flutter/lib/channel.dart.twig @@ -116,7 +116,7 @@ abstract class Channel { static ITablesDB tablesdb([String id = '*']) => _ChannelBuilder._(['tablesdb', _normalize(id)]); - static IBucket buckets([String id = '*']) => + static IBucket bucket([String id = '*']) => _ChannelBuilder._(['buckets', _normalize(id)]); static IFunction function([String id = '*']) => diff --git a/templates/flutter/lib/src/realtime.dart.twig b/templates/flutter/lib/src/realtime.dart.twig index fef7cc0d7a..14de266332 100644 --- a/templates/flutter/lib/src/realtime.dart.twig +++ b/templates/flutter/lib/src/realtime.dart.twig @@ -46,7 +46,7 @@ abstract class Realtime extends Service { /// ```dart /// final subscription = realtime.subscribe([ /// Channel.database('db').collection('col').document('doc').create(), - /// Channel.buckets('bucket').file('file').update(), + /// Channel.bucket('bucket').file('file').update(), /// 'account.*' /// ]); /// ``` diff --git a/templates/flutter/test/src/channel_test.dart.twig b/templates/flutter/test/src/channel_test.dart.twig index 0ff2b2a9e4..968b67a827 100644 --- a/templates/flutter/test/src/channel_test.dart.twig +++ b/templates/flutter/test/src/channel_test.dart.twig @@ -44,18 +44,18 @@ void main() { }); }); - group('buckets()', () { + group('bucket()', () { test('returns buckets channel with defaults', () { - expect(Channel.buckets().file().toString(), 'buckets.*.files.*'); + expect(Channel.bucket().file().toString(), 'buckets.*.files.*'); }); test('returns buckets channel with specific IDs', () { - expect(Channel.buckets('bucket1').file('file1').toString(), + expect(Channel.bucket('bucket1').file('file1').toString(), 'buckets.bucket1.files.file1'); }); test('returns buckets channel with action', () { - expect(Channel.buckets('bucket1').file('file1').delete().toString(), + expect(Channel.bucket('bucket1').file('file1').delete().toString(), 'buckets.bucket1.files.file1.delete'); }); }); diff --git a/templates/react-native/src/channel.ts.twig b/templates/react-native/src/channel.ts.twig index b22874b88e..388e1dbf30 100644 --- a/templates/react-native/src/channel.ts.twig +++ b/templates/react-native/src/channel.ts.twig @@ -153,7 +153,7 @@ class ResolvedChannel { return ChannelBuilder.start(["tablesdb", normalize(id)]); } - static buckets(id: string = "*"): IBucket { + static bucket(id: string = "*"): IBucket { return ChannelBuilder.start(["buckets", normalize(id)]); } diff --git a/templates/web/src/channel.ts.twig b/templates/web/src/channel.ts.twig index 8446bfda17..0b3c4da372 100644 --- a/templates/web/src/channel.ts.twig +++ b/templates/web/src/channel.ts.twig @@ -153,7 +153,7 @@ export class ResolvedChannel { return ChannelBuilder.start(["tablesdb", normalize(id)]); } - static buckets(id: string = "*"): IBucket { + static bucket(id: string = "*"): IBucket { return ChannelBuilder.start(["buckets", normalize(id)]); } diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index a2011f8634..4ead22f291 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -575,7 +575,7 @@ class Client { * * You can also use Channel builders: * - Channel.database('db').collection('col').document('doc').create() - * - Channel.buckets('bucket').file('file').update() + * - Channel.bucket('bucket').file('file').update() * - Channel.function('func').execution('exec').delete() * - Channel.team('team').create() * - Channel.membership('membership').update() diff --git a/tests/languages/android/Tests.kt b/tests/languages/android/Tests.kt index 5f775cf064..714b2e6dba 100644 --- a/tests/languages/android/Tests.kt +++ b/tests/languages/android/Tests.kt @@ -279,9 +279,9 @@ class ServiceTest { writeToFile(Channel.tablesdb("db1").table("table1").row("row1").update().toString()) writeToFile(Channel.account()) writeToFile(Channel.account("user123")) - writeToFile(Channel.buckets().file().toString()) - writeToFile(Channel.buckets("bucket1").file("file1").toString()) - writeToFile(Channel.buckets("bucket1").file("file1").delete().toString()) + writeToFile(Channel.bucket().file().toString()) + writeToFile(Channel.bucket("bucket1").file("file1").toString()) + writeToFile(Channel.bucket("bucket1").file("file1").delete().toString()) writeToFile(Channel.function().execution().toString()) writeToFile(Channel.function("func1").execution("exec1").toString()) writeToFile(Channel.function("func1").execution("exec1").create().toString()) diff --git a/tests/languages/apple/Tests.swift b/tests/languages/apple/Tests.swift index 4b1324dce8..3ff6598236 100644 --- a/tests/languages/apple/Tests.swift +++ b/tests/languages/apple/Tests.swift @@ -257,9 +257,9 @@ class Tests: XCTestCase { print(Channel.tablesdb("db1").table("table1").row("row1").update().toString()) print(Channel.account()) print(Channel.account("user123")) - print(Channel.buckets().file().toString()) - print(Channel.buckets("bucket1").file("file1").toString()) - print(Channel.buckets("bucket1").file("file1").delete().toString()) + print(Channel.bucket().file().toString()) + print(Channel.bucket("bucket1").file("file1").toString()) + print(Channel.bucket("bucket1").file("file1").delete().toString()) print(Channel.function().execution().toString()) print(Channel.function("func1").execution("exec1").toString()) print(Channel.function("func1").execution("exec1").create().toString()) diff --git a/tests/languages/flutter/tests.dart b/tests/languages/flutter/tests.dart index 6592176e67..6c3ef76dec 100644 --- a/tests/languages/flutter/tests.dart +++ b/tests/languages/flutter/tests.dart @@ -256,9 +256,9 @@ void main() async { print(Channel.tablesdb('db1').table('table1').row('row1').update().toString()); print(Channel.account()); print(Channel.account('user123')); - print(Channel.buckets().file().toString()); - print(Channel.buckets('bucket1').file('file1').toString()); - print(Channel.buckets('bucket1').file('file1').delete().toString()); + print(Channel.bucket().file().toString()); + print(Channel.bucket('bucket1').file('file1').toString()); + print(Channel.bucket('bucket1').file('file1').delete().toString()); print(Channel.function().execution().toString()); print(Channel.function('func1').execution('exec1').toString()); print(Channel.function('func1').execution('exec1').create().toString()); diff --git a/tests/languages/web/index.html b/tests/languages/web/index.html index d7430a95b5..e83a23f9e3 100644 --- a/tests/languages/web/index.html +++ b/tests/languages/web/index.html @@ -328,9 +328,9 @@ console.log(Channel.tablesdb('db1').table('table1').row('row1').update().toString()); console.log(Channel.account()); console.log(Channel.account('user123')); - console.log(Channel.buckets().file().toString()); - console.log(Channel.buckets('bucket1').file('file1').toString()); - console.log(Channel.buckets('bucket1').file('file1').delete().toString()); + console.log(Channel.bucket().file().toString()); + console.log(Channel.bucket('bucket1').file('file1').toString()); + console.log(Channel.bucket('bucket1').file('file1').delete().toString()); console.log(Channel.function().execution().toString()); console.log(Channel.function('func1').execution('exec1').toString()); console.log(Channel.function('func1').execution('exec1').create().toString()); diff --git a/tests/languages/web/node.js b/tests/languages/web/node.js index 6ee93e0f98..1cead4ca6e 100644 --- a/tests/languages/web/node.js +++ b/tests/languages/web/node.js @@ -259,9 +259,9 @@ async function start() { console.log(Channel.tablesdb('db1').table('table1').row('row1').update().toString()); console.log(Channel.account()); console.log(Channel.account('user123')); - console.log(Channel.buckets().file().toString()); - console.log(Channel.buckets('bucket1').file('file1').toString()); - console.log(Channel.buckets('bucket1').file('file1').delete().toString()); + console.log(Channel.bucket().file().toString()); + console.log(Channel.bucket('bucket1').file('file1').toString()); + console.log(Channel.bucket('bucket1').file('file1').delete().toString()); console.log(Channel.function().execution().toString()); console.log(Channel.function('func1').execution('exec1').toString()); console.log(Channel.function('func1').execution('exec1').create().toString()); From 7571e2d2b0ebcd62c7a1b5e9c72f215458bc6b0d Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 9 Jan 2026 14:30:38 +0530 Subject: [PATCH 14/26] updated react native and web ts --- templates/react-native/src/channel.ts.twig | 6 +++--- templates/web/src/channel.ts.twig | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/react-native/src/channel.ts.twig b/templates/react-native/src/channel.ts.twig index 388e1dbf30..d17e86d139 100644 --- a/templates/react-native/src/channel.ts.twig +++ b/templates/react-native/src/channel.ts.twig @@ -157,15 +157,15 @@ class ResolvedChannel { return ChannelBuilder.start(["buckets", normalize(id)]); } - static functions(id: string = "*"): IFunction { + static function(id: string = "*"): IFunction { return ChannelBuilder.start(["functions", normalize(id)]); } - static teams(id: string = "*"): Actionable { + static team(id: string = "*"): Actionable { return ChannelBuilder.start(["teams", normalize(id)]); } - static memberships(id: string = "*"): Actionable { + static membership(id: string = "*"): Actionable { return ChannelBuilder.start(["memberships", normalize(id)]); } diff --git a/templates/web/src/channel.ts.twig b/templates/web/src/channel.ts.twig index 0b3c4da372..33e5968535 100644 --- a/templates/web/src/channel.ts.twig +++ b/templates/web/src/channel.ts.twig @@ -157,7 +157,7 @@ export class ResolvedChannel { return ChannelBuilder.start(["buckets", normalize(id)]); } - static function(id: string = "*"): IFunction { + static function(id: string = "*"): IFunction { return ChannelBuilder.start(["functions", normalize(id)]); } From 2ed26c9b64a5a779da1066c11147b61bc1ec9e43 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 9 Jan 2026 14:31:08 +0530 Subject: [PATCH 15/26] updated channel and resolved channel validation in web --- templates/web/src/client.ts.twig | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index 4ead22f291..b611d15939 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -589,14 +589,15 @@ class Client { if (typeof ch === 'string') { return ch; } - // Check if it's an Actionable instance (has channel() method) + // Check if it's a ResolvedChannel instance first + if (ch && ch instanceof ResolvedChannel) { + return (ch as ResolvedChannel).toString(); + } + // Then check if it's an Actionable instance (has channel() method) if (ch && typeof (ch as Actionable).channel === 'function') { return (ch as Actionable).channel(); } - // Check if it's a ResolvedChannel (has toString() method) - if (ch && typeof (ch as ResolvedChannel).toString === 'function') { - return (ch as ResolvedChannel).toString(); - } + // Fallback to generic string conversion return String(ch); }); channelStrings.forEach(channel => this.realtime.channels.add(channel)); From 77147bd328b6cd1698587f44361dca42165c890f Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 9 Jan 2026 14:51:18 +0530 Subject: [PATCH 16/26] added overloading for type strictness in kotlin --- .../java/io/package/services/Realtime.kt.twig | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig b/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig index 79e509a788..4e0efea336 100644 --- a/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig +++ b/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig @@ -127,7 +127,16 @@ class Realtime(client: Client) : Service(client), CoroutineScope { } fun subscribe( - vararg channels: Any, + vararg channels: Actionable, + callback: (RealtimeResponseEvent) -> Unit, + ) = subscribe( + channels = channels.map { channelToString(it) }.toTypedArray(), + Any::class.java, + callback + ) + + fun subscribe( + vararg channels: ResolvedChannel, callback: (RealtimeResponseEvent) -> Unit, ) = subscribe( channels = channels.map { channelToString(it) }.toTypedArray(), @@ -145,7 +154,19 @@ class Realtime(client: Client) : Service(client), CoroutineScope { ) fun subscribe( - vararg channels: Any, + vararg channels: Actionable, + payloadType: Class, + callback: (RealtimeResponseEvent) -> Unit, + ): RealtimeSubscription { + return subscribe( + channels = channels.map { channelToString(it) }.toTypedArray(), + payloadType = payloadType, + callback = callback + ) + } + + fun subscribe( + vararg channels: ResolvedChannel, payloadType: Class, callback: (RealtimeResponseEvent) -> Unit, ): RealtimeSubscription { From 6b30e48e9548641947cfa37a30f0a0ca10e62720 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 9 Jan 2026 15:35:27 +0530 Subject: [PATCH 17/26] removed leftovers --- tests/AppleSwift56Test.php | 118 ------------------------------------- 1 file changed, 118 deletions(-) diff --git a/tests/AppleSwift56Test.php b/tests/AppleSwift56Test.php index ddd482f604..3d4ae732e2 100644 --- a/tests/AppleSwift56Test.php +++ b/tests/AppleSwift56Test.php @@ -34,122 +34,4 @@ class AppleSwift56Test extends Base ...Base::CHANNEL_HELPER_RESPONSES, ...Base::OPERATOR_HELPER_RESPONSES ]; - - /** - * Override to log all output including compilation errors - */ - public function testHTTPSuccess(): void - { - $spec = file_get_contents(realpath(__DIR__ . '/resources/spec.json')); - - if (empty($spec)) { - throw new \Exception('Failed to parse spec.'); - } - - $sdk = new \Appwrite\SDK\SDK($this->getLanguage(), new \Appwrite\Spec\Swagger2($spec)); - - $sdk - ->setName($this->sdkName) - ->setVersion($this->version) - ->setPlatform($this->sdkPlatform) - ->setDescription('Repo description goes here') - ->setShortDescription('Repo short description goes here') - ->setLogo('https://appwrite.io/v1/images/console.png') - ->setWarning('**WORK IN PROGRESS - THIS IS JUST A TEST SDK**') - ->setExamples('**EXAMPLES** ') - ->setNamespace("io appwrite") - ->setGitUserName('repoowner') - ->setGitRepoName('reponame') - ->setLicense('BSD-3-Clause') - ->setLicenseContent('demo license') - ->setChangelog('--changelog--') - ->setDefaultHeaders([ - 'X-Appwrite-Response-Format' => '0.8.0', - ]) - ->setTest("true"); - - $dir = __DIR__ . '/sdks/' . $this->language; - - $this->rmdirRecursive($dir); - - $sdk->generate(__DIR__ . '/sdks/' . $this->language); - - /** - * Build SDK - */ - foreach ($this->build as $command) { - echo "Build Executing: {$command}\n"; - exec($command); - } - - $output = []; - $returnVar = 0; - - echo "Env Executing: {$this->command}\n"; - - // Capture both stdout and stderr, and return code - exec($this->command, $output, $returnVar); - - // Log ALL output including compilation errors - echo "\n=== FULL OUTPUT (including errors) ===\n"; - echo implode("\n", $output); - echo "\n=== END FULL OUTPUT ===\n"; - echo "Return code: {$returnVar}\n\n"; - - $this->assertIsArray($output); - - // Save original output for error logging - $originalOutput = $output; - - do { - $removed = \array_shift($output); - } while ($removed != 'Test Started' && sizeof($output) != 0); - - // If we filtered everything out, it means "Test Started" was never found - // This likely means there were compilation errors - if (sizeof($output) == 0 && sizeof($originalOutput) > 0) { - echo "\n=== ERROR: 'Test Started' marker not found. Full output above shows compilation errors. ===\n"; - } - - echo \implode("\n", $output); - - foreach ($this->expectedOutput as $index => $expected) { - if (!isset($output[$index])) { - echo "\n=== ERROR: Missing output at index {$index}. Expected: {$expected} ===\n"; - continue; - } - // HACK: Swift does not guarantee the order of the JSON parameters - if (\str_starts_with($expected, '{')) { - $this->assertEquals( - \json_decode($expected, true), - \json_decode($output[$index], true) - ); - } elseif ($expected == 'unique()') { - $this->assertNotEmpty($output[$index]); - $this->assertIsString($output[$index]); - $this->assertEquals(20, strlen($output[$index])); - $this->assertNotEquals($output[$index], 'unique()'); - } else { - $this->assertEquals($expected, $output[$index]); - } - } - } - - private function rmdirRecursive($dir): void - { - if (!\is_dir($dir)) { - return; - } - foreach (\scandir($dir) as $file) { - if ('.' === $file || '..' === $file) { - continue; - } - if (\is_dir("$dir/$file")) { - $this->rmdirRecursive("$dir/$file"); - } else { - \unlink("$dir/$file"); - } - } - \rmdir($dir); - } } From cd889741f6dad73fae52c747f8e8679245875ae3 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 9 Jan 2026 16:10:36 +0530 Subject: [PATCH 18/26] updated leftover --- .../library/src/main/java/io/package/Channel.kt.twig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/android/library/src/main/java/io/package/Channel.kt.twig b/templates/android/library/src/main/java/io/package/Channel.kt.twig index 61d7ea7842..133b04f23a 100644 --- a/templates/android/library/src/main/java/io/package/Channel.kt.twig +++ b/templates/android/library/src/main/java/io/package/Channel.kt.twig @@ -32,10 +32,10 @@ interface Database { } interface Collection { - fun document(id: String = "*"): IDocument + fun document(id: String = "*"): Document } -interface IDocument : Actionable +interface Document : Actionable interface TablesDB { fun table(id: String = "*"): Table @@ -61,7 +61,7 @@ interface Execution : Actionable private class ChannelBuilder( private val segments: List -) : Database, Collection, IDocument, +) : Database, Collection, Document, TablesDB, Table, Row, Bucket, File, Function, Execution, Actionable { @@ -77,7 +77,7 @@ private class ChannelBuilder( override fun collection(id: String): Collection = next("collections", id) - override fun document(id: String): IDocument = + override fun document(id: String): Document = next("documents", id) override fun table(id: String): Table = From d24815582f2ccf30a0889fff79aaeae639be6309 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 9 Jan 2026 16:39:05 +0530 Subject: [PATCH 19/26] fixed failing swift tests --- templates/apple/Sources/Channel.swift.twig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/templates/apple/Sources/Channel.swift.twig b/templates/apple/Sources/Channel.swift.twig index cfd5a8773d..1420004be0 100644 --- a/templates/apple/Sources/Channel.swift.twig +++ b/templates/apple/Sources/Channel.swift.twig @@ -41,6 +41,13 @@ public protocol Actionable: ChannelValue { func delete() -> ResolvedChannel } +// Provide default toString() implementation for Actionable +extension Actionable { + public func toString() -> String { + return channel + } +} + public protocol IDatabase { func collection(_ id: String) -> ICollection } From b0533b1a0124fbaddb4bbd4f92e3e8b08cf76e93 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Fri, 9 Jan 2026 16:39:05 +0530 Subject: [PATCH 20/26] fixed failing swift tests --- templates/apple/Sources/Channel.swift.twig | 7 +++++++ tests/AppleSwift56Test.php | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/templates/apple/Sources/Channel.swift.twig b/templates/apple/Sources/Channel.swift.twig index cfd5a8773d..1420004be0 100644 --- a/templates/apple/Sources/Channel.swift.twig +++ b/templates/apple/Sources/Channel.swift.twig @@ -41,6 +41,13 @@ public protocol Actionable: ChannelValue { func delete() -> ResolvedChannel } +// Provide default toString() implementation for Actionable +extension Actionable { + public func toString() -> String { + return channel + } +} + public protocol IDatabase { func collection(_ id: String) -> ICollection } diff --git a/tests/AppleSwift56Test.php b/tests/AppleSwift56Test.php index 3d4ae732e2..36852cae07 100644 --- a/tests/AppleSwift56Test.php +++ b/tests/AppleSwift56Test.php @@ -16,7 +16,7 @@ class AppleSwift56Test extends Base 'cp tests/languages/apple/Tests.swift tests/sdks/apple/Tests/AppwriteTests/Tests.swift', ]; protected string $command = - 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/apple swift:5.6-focal swift test 2>&1'; + 'docker run --network="mockapi" --rm -v $(pwd):/app -w /app/tests/sdks/apple swift:5.6-focal swift test'; protected array $expectedOutput = [ ...Base::PING_RESPONSE, From 18152a15cc37c2fe1051f93bbbf6f9cc763b3d74 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 12 Jan 2026 20:34:48 +0530 Subject: [PATCH 21/26] revamped realtime channel support for the js realtime --- templates/react-native/src/channel.ts.twig | 327 ++++++++------------ templates/web/src/channel.ts.twig | 327 ++++++++------------ templates/web/src/client.ts.twig | 16 +- templates/web/src/services/realtime.ts.twig | 34 +- 4 files changed, 287 insertions(+), 417 deletions(-) diff --git a/templates/react-native/src/channel.ts.twig b/templates/react-native/src/channel.ts.twig index d17e86d139..1d68ded770 100644 --- a/templates/react-native/src/channel.ts.twig +++ b/templates/react-native/src/channel.ts.twig @@ -1,195 +1,134 @@ -// The result of building the channel. -class ResolvedChannel { - constructor(private value: string) {} - toString() { - return this.value; - } - } - - // Helper function for normalizing ID - function normalize(id: string): string { - const trimmed = id.trim(); - return trimmed === "" ? "*" : trimmed; - } - - // State interfaces - - interface Actionable { - create(): ResolvedChannel; - update(): ResolvedChannel; - delete(): ResolvedChannel; - channel(): string; - } - - interface IDatabase { - collection(id?: string): ICollection; - } - - interface ICollection { - document(id?: string): IDocument; - } - - interface IDocument extends Actionable {} - - interface ITablesDB { - table(id?: string): ITable; - } - - interface ITable { - row(id?: string): IRow; - } - - interface IRow extends Actionable {} - - interface IBucket { - file(id?: string): IFile; - } - - interface IFile extends Actionable {} - - interface IFunction { - execution(id?: string): IExecution; - } - - interface IExecution extends Actionable {} - - // Builder implementation - - class ChannelBuilder - implements - IDatabase, - ICollection, - IDocument, - ITablesDB, - ITable, - IRow, - IBucket, - IFile, - IFunction, - IExecution, - Actionable - { - constructor(private segments: string[]) {} - - static start(segments: string[]): ChannelBuilder { - return new ChannelBuilder(segments); - } - - channel(): string { - return this.segments.join("."); - } - - toString(): string { - return this.channel(); - } - - private next(segment: string, id: string = "*"): ChannelBuilder { - return new ChannelBuilder([ - ...this.segments, - segment, - normalize(id), - ]); - } - - private resolve(action: string): ResolvedChannel { - return new ResolvedChannel(`${this.channel()}.${action}`); - } - - // Database - - collection(id: string = "*"): ICollection { - return this.next("collections", id); - } - - document(id: string = "*"): IDocument { - return this.next("documents", id); - } - - // TablesDB - - table(id: string = "*"): ITable { - return this.next("tables", id); - } - - row(id: string = "*"): IRow { - return this.next("rows", id); - } - - // Buckets - - file(id: string = "*"): IFile { - return this.next("files", id); - } - - // Functions - - execution(id: string = "*"): IExecution { - return this.next("executions", id); - } - - // Actions - - create(): ResolvedChannel { - return this.resolve("create"); - } - - update(): ResolvedChannel { - return this.resolve("update"); - } - - delete(): ResolvedChannel { - return this.resolve("delete"); - } - } - - // Entry point - - export class Channel { - static database(id: string = "*"): IDatabase { - return ChannelBuilder.start(["databases", normalize(id)]); - } - - static tablesdb(id: string = "*"): ITablesDB { - return ChannelBuilder.start(["tablesdb", normalize(id)]); - } - - static bucket(id: string = "*"): IBucket { - return ChannelBuilder.start(["buckets", normalize(id)]); - } - - static function(id: string = "*"): IFunction { - return ChannelBuilder.start(["functions", normalize(id)]); - } - - static team(id: string = "*"): Actionable { - return ChannelBuilder.start(["teams", normalize(id)]); - } - - static membership(id: string = "*"): Actionable { - return ChannelBuilder.start(["memberships", normalize(id)]); - } - - static account(userId: string = ""): string { - return userId.trim() - ? `account.${normalize(userId)}` - : "account"; - } - - // Global events - - static get documents(): string { - return "documents"; - } - - static get rows(): string { - return "rows"; - } - - static get files(): string { - return "files"; - } - - static get executions(): string { - return "executions"; - } +interface Root {} +interface Database { _db: any } +interface Collection { _coll: any } +interface Document { _doc: any } +interface TablesDB { _tdb: any } +interface Table { _tbl: any } +interface Row { _row: any } +interface Bucket { _bkt: any } +interface File { _file: any } +interface Func { _fn: any } +interface Execution { _exec: any } +interface Team { _team: any } +interface Membership { _mem: any } +interface Resolved { _res: any } + +type Actionable = Document | Row | File | Execution | Team | Membership; + +function normalize(id: string): string { + const trimmed = id.trim(); + return trimmed === "" ? "*" : trimmed; +} + +export class Channel { + declare _type: T; + + private constructor(private readonly segments: string[]) {} + + private next(segment: string, id: string = "*"): Channel { + return new Channel([...this.segments, segment, normalize(id)]) as any; } + + private resolve(action: string): Channel { + return new Channel([...this.segments, action]) as any; + } + + toString(): string { + return this.segments.join("."); + } + + // --- DATABASE ROUTE --- + // Only available on Channel + collection(this: Channel, id: string = "*"): Channel { + return this.next("collections", id); + } + + // Only available on Channel + document(this: Channel, id: string = "*"): Channel { + return this.next("documents", id); + } + + // --- TABLESDB ROUTE --- + table(this: Channel, id: string = "*"): Channel { + return this.next
("tables", id); + } + + row(this: Channel
, id: string = "*"): Channel { + return this.next("rows", id); + } + + // --- BUCKET ROUTE --- + file(this: Channel, id: string = "*"): Channel { + return this.next("files", id); + } + + // --- FUNCTION ROUTE --- + execution(this: Channel, id: string = "*"): Channel { + return this.next("executions", id); + } + + // --- TERMINAL ACTIONS --- + // Restricted to the Actionable union + create(this: Channel): Channel { + return this.resolve("create"); + } + + update(this: Channel): Channel { + return this.resolve("update"); + } + + delete(this: Channel): Channel { + return this.resolve("delete"); + } + + // --- ROOT FACTORIES --- + static database(id: string = "*") { + return new Channel(["databases", normalize(id)]); + } + + static tablesdb(id: string = "*") { + return new Channel(["tablesdb", normalize(id)]); + } + + static bucket(id: string = "*") { + return new Channel(["buckets", normalize(id)]); + } + + static function(id: string = "*") { + return new Channel(["functions", normalize(id)]); + } + + static team(id: string = "*") { + return new Channel(["teams", normalize(id)]); + } + + static membership(id: string = "*") { + return new Channel(["memberships", normalize(id)]); + } + + static account(userId: string = ""): string { + const id = normalize(userId); + return id === "*" ? "account" : `account.${id}`; + } + + // Global events + static get documents(): string { + return "documents"; + } + + static get rows(): string { + return "rows"; + } + + static get files(): string { + return "files"; + } + + static get executions(): string { + return "executions"; + } +} + +// Export types for backward compatibility with realtime +export type ActionableChannel = Channel | Channel | Channel | Channel | Channel | Channel; +export type ResolvedChannel = Channel; diff --git a/templates/web/src/channel.ts.twig b/templates/web/src/channel.ts.twig index 33e5968535..1d68ded770 100644 --- a/templates/web/src/channel.ts.twig +++ b/templates/web/src/channel.ts.twig @@ -1,195 +1,134 @@ -// The result of building the channel. -export class ResolvedChannel { - constructor(private value: string) {} - toString() { - return this.value; - } - } - - // Helper function for normalizing ID - function normalize(id: string): string { - const trimmed = id.trim(); - return trimmed === "" ? "*" : trimmed; - } - - // State interfaces - - export interface Actionable { - create(): ResolvedChannel; - update(): ResolvedChannel; - delete(): ResolvedChannel; - channel(): string; - } - - interface IDatabase { - collection(id?: string): ICollection; - } - - interface ICollection { - document(id?: string): IDocument; - } - - interface IDocument extends Actionable {} - - interface ITablesDB { - table(id?: string): ITable; - } - - interface ITable { - row(id?: string): IRow; - } - - interface IRow extends Actionable {} - - interface IBucket { - file(id?: string): IFile; - } - - interface IFile extends Actionable {} - - interface IFunction { - execution(id?: string): IExecution; - } - - interface IExecution extends Actionable {} - - // Builder implementation - - class ChannelBuilder - implements - IDatabase, - ICollection, - IDocument, - ITablesDB, - ITable, - IRow, - IBucket, - IFile, - IFunction, - IExecution, - Actionable - { - constructor(private segments: string[]) {} - - static start(segments: string[]): ChannelBuilder { - return new ChannelBuilder(segments); - } - - channel(): string { - return this.segments.join("."); - } - - toString(): string { - return this.channel(); - } - - private next(segment: string, id: string = "*"): ChannelBuilder { - return new ChannelBuilder([ - ...this.segments, - segment, - normalize(id), - ]); - } - - private resolve(action: string): ResolvedChannel { - return new ResolvedChannel(`${this.channel()}.${action}`); - } - - // Database - - collection(id: string = "*"): ICollection { - return this.next("collections", id); - } - - document(id: string = "*"): IDocument { - return this.next("documents", id); - } - - // TablesDB - - table(id: string = "*"): ITable { - return this.next("tables", id); - } - - row(id: string = "*"): IRow { - return this.next("rows", id); - } - - // Buckets - - file(id: string = "*"): IFile { - return this.next("files", id); - } - - // Functions - - execution(id: string = "*"): IExecution { - return this.next("executions", id); - } - - // Actions - - create(): ResolvedChannel { - return this.resolve("create"); - } - - update(): ResolvedChannel { - return this.resolve("update"); - } - - delete(): ResolvedChannel { - return this.resolve("delete"); - } - } - - // Entry point - - export class Channel { - static database(id: string = "*"): IDatabase { - return ChannelBuilder.start(["databases", normalize(id)]); - } - - static tablesdb(id: string = "*"): ITablesDB { - return ChannelBuilder.start(["tablesdb", normalize(id)]); - } - - static bucket(id: string = "*"): IBucket { - return ChannelBuilder.start(["buckets", normalize(id)]); - } - - static function(id: string = "*"): IFunction { - return ChannelBuilder.start(["functions", normalize(id)]); - } - - static team(id: string = "*"): Actionable { - return ChannelBuilder.start(["teams", normalize(id)]); - } - - static membership(id: string = "*"): Actionable { - return ChannelBuilder.start(["memberships", normalize(id)]); - } - - static account(userId: string = ""): string { - return userId.trim() - ? `account.${normalize(userId)}` - : "account"; - } - - // Global events - - static get documents(): string { - return "documents"; - } - - static get rows(): string { - return "rows"; - } - - static get files(): string { - return "files"; - } - - static get executions(): string { - return "executions"; - } +interface Root {} +interface Database { _db: any } +interface Collection { _coll: any } +interface Document { _doc: any } +interface TablesDB { _tdb: any } +interface Table { _tbl: any } +interface Row { _row: any } +interface Bucket { _bkt: any } +interface File { _file: any } +interface Func { _fn: any } +interface Execution { _exec: any } +interface Team { _team: any } +interface Membership { _mem: any } +interface Resolved { _res: any } + +type Actionable = Document | Row | File | Execution | Team | Membership; + +function normalize(id: string): string { + const trimmed = id.trim(); + return trimmed === "" ? "*" : trimmed; +} + +export class Channel { + declare _type: T; + + private constructor(private readonly segments: string[]) {} + + private next(segment: string, id: string = "*"): Channel { + return new Channel([...this.segments, segment, normalize(id)]) as any; } + + private resolve(action: string): Channel { + return new Channel([...this.segments, action]) as any; + } + + toString(): string { + return this.segments.join("."); + } + + // --- DATABASE ROUTE --- + // Only available on Channel + collection(this: Channel, id: string = "*"): Channel { + return this.next("collections", id); + } + + // Only available on Channel + document(this: Channel, id: string = "*"): Channel { + return this.next("documents", id); + } + + // --- TABLESDB ROUTE --- + table(this: Channel, id: string = "*"): Channel
{ + return this.next
("tables", id); + } + + row(this: Channel
, id: string = "*"): Channel { + return this.next("rows", id); + } + + // --- BUCKET ROUTE --- + file(this: Channel, id: string = "*"): Channel { + return this.next("files", id); + } + + // --- FUNCTION ROUTE --- + execution(this: Channel, id: string = "*"): Channel { + return this.next("executions", id); + } + + // --- TERMINAL ACTIONS --- + // Restricted to the Actionable union + create(this: Channel): Channel { + return this.resolve("create"); + } + + update(this: Channel): Channel { + return this.resolve("update"); + } + + delete(this: Channel): Channel { + return this.resolve("delete"); + } + + // --- ROOT FACTORIES --- + static database(id: string = "*") { + return new Channel(["databases", normalize(id)]); + } + + static tablesdb(id: string = "*") { + return new Channel(["tablesdb", normalize(id)]); + } + + static bucket(id: string = "*") { + return new Channel(["buckets", normalize(id)]); + } + + static function(id: string = "*") { + return new Channel(["functions", normalize(id)]); + } + + static team(id: string = "*") { + return new Channel(["teams", normalize(id)]); + } + + static membership(id: string = "*") { + return new Channel(["memberships", normalize(id)]); + } + + static account(userId: string = ""): string { + const id = normalize(userId); + return id === "*" ? "account" : `account.${id}`; + } + + // Global events + static get documents(): string { + return "documents"; + } + + static get rows(): string { + return "rows"; + } + + static get files(): string { + return "files"; + } + + static get executions(): string { + return "executions"; + } +} + +// Export types for backward compatibility with realtime +export type ActionableChannel = Channel | Channel | Channel | Channel | Channel | Channel; +export type ResolvedChannel = Channel; diff --git a/templates/web/src/client.ts.twig b/templates/web/src/client.ts.twig index b611d15939..24ccd609d4 100644 --- a/templates/web/src/client.ts.twig +++ b/templates/web/src/client.ts.twig @@ -1,5 +1,5 @@ import { Models } from './models'; -import { Actionable, ResolvedChannel } from './channel'; +import { Channel, ActionableChannel, ResolvedChannel } from './channel'; /** * Payload type representing a key-value pair with string keys and any values. @@ -553,7 +553,7 @@ class Client { * @deprecated Use the Realtime service instead. * @see Realtime * - * @param {string|string[]|Actionable|ResolvedChannel|(Actionable|ResolvedChannel)[]} channels + * @param {string|string[]|Channel|ActionableChannel|ResolvedChannel|(Channel|ActionableChannel|ResolvedChannel)[]} channels * Channel to subscribe - pass a single channel as a string or Channel builder instance, or multiple with an array. * * Possible channels are: @@ -582,20 +582,16 @@ class Client { * @param {(payload: RealtimeMessage) => void} callback Is called on every realtime update. * @returns {() => void} Unsubscribes from events. */ - subscribe(channels: string | string[] | Actionable | ResolvedChannel | (Actionable | ResolvedChannel)[], callback: (payload: RealtimeResponseEvent) => void): () => void { + subscribe(channels: string | string[] | Channel | ActionableChannel | ResolvedChannel | (Channel | ActionableChannel | ResolvedChannel)[], callback: (payload: RealtimeResponseEvent) => void): () => void { const channelArray = Array.isArray(channels) ? channels : [channels]; // Convert Channel instances to strings const channelStrings = channelArray.map(ch => { if (typeof ch === 'string') { return ch; } - // Check if it's a ResolvedChannel instance first - if (ch && ch instanceof ResolvedChannel) { - return (ch as ResolvedChannel).toString(); - } - // Then check if it's an Actionable instance (has channel() method) - if (ch && typeof (ch as Actionable).channel === 'function') { - return (ch as Actionable).channel(); + // All Channel instances have toString() method + if (ch && typeof (ch as Channel).toString === 'function') { + return (ch as Channel).toString(); } // Fallback to generic string conversion return String(ch); diff --git a/templates/web/src/services/realtime.ts.twig b/templates/web/src/services/realtime.ts.twig index 6d99fd8f4a..009f274d8e 100644 --- a/templates/web/src/services/realtime.ts.twig +++ b/templates/web/src/services/realtime.ts.twig @@ -1,5 +1,5 @@ import { {{ spec.title | caseUcfirst}}Exception, Client } from '../client'; -import { Actionable, ResolvedChannel } from '../channel'; +import { Channel, ActionableChannel, ResolvedChannel } from '../channel'; export type RealtimeSubscription = { close: () => Promise; @@ -242,20 +242,16 @@ export class Realtime { * Convert a channel value to a string * * @private - * @param {string | Actionable | ResolvedChannel} channel - Channel value (string or Channel builder instance) + * @param {string | Channel | ActionableChannel | ResolvedChannel} channel - Channel value (string or Channel builder instance) * @returns {string} Channel string representation */ - private channelToString(channel: string | Actionable | ResolvedChannel): string { + private channelToString(channel: string | Channel | ActionableChannel | ResolvedChannel): string { if (typeof channel === 'string') { return channel; } - // Check if it's an Actionable instance (has channel() method) - if (channel && typeof (channel as Actionable).channel === 'function') { - return (channel as Actionable).channel(); - } - // Check if it's a ResolvedChannel (has toString() method) - if (channel && typeof (channel as ResolvedChannel).toString === 'function') { - return (channel as ResolvedChannel).toString(); + // All Channel instances have toString() method + if (channel && typeof (channel as Channel).toString === 'function') { + return (channel as Channel).toString(); } return String(channel); } @@ -263,53 +259,53 @@ export class Realtime { /** * Subscribe to a single channel * - * @param {string | Actionable | ResolvedChannel} channel - Channel name to subscribe to (string or Channel builder instance) + * @param {string | Channel | ActionableChannel | ResolvedChannel} channel - Channel name to subscribe to (string or Channel builder instance) * @param {Function} callback - Callback function to handle events * @returns {Promise} Subscription object with close method */ public async subscribe( - channel: string | Actionable | ResolvedChannel, + channel: string | Channel | ActionableChannel | ResolvedChannel, callback: (event: RealtimeResponseEvent) => void ): Promise; /** * Subscribe to multiple channels * - * @param {(string | Actionable | ResolvedChannel)[]} channels - Array of channel names to subscribe to (strings or Channel builder instances) + * @param {(string | Channel | ActionableChannel | ResolvedChannel)[]} channels - Array of channel names to subscribe to (strings or Channel builder instances) * @param {Function} callback - Callback function to handle events * @returns {Promise} Subscription object with close method */ public async subscribe( - channels: (string | Actionable | ResolvedChannel)[], + channels: (string | Channel | ActionableChannel | ResolvedChannel)[], callback: (event: RealtimeResponseEvent) => void ): Promise; /** * Subscribe to a single channel with typed payload * - * @param {string | Actionable | ResolvedChannel} channel - Channel name to subscribe to (string or Channel builder instance) + * @param {string | Channel | ActionableChannel | ResolvedChannel} channel - Channel name to subscribe to (string or Channel builder instance) * @param {Function} callback - Callback function to handle events with typed payload * @returns {Promise} Subscription object with close method */ public async subscribe( - channel: string | Actionable | ResolvedChannel, + channel: string | Channel | ActionableChannel | ResolvedChannel, callback: (event: RealtimeResponseEvent) => void ): Promise; /** * Subscribe to multiple channels with typed payload * - * @param {(string | Actionable | ResolvedChannel)[]} channels - Array of channel names to subscribe to (strings or Channel builder instances) + * @param {(string | Channel | ActionableChannel | ResolvedChannel)[]} channels - Array of channel names to subscribe to (strings or Channel builder instances) * @param {Function} callback - Callback function to handle events with typed payload * @returns {Promise} Subscription object with close method */ public async subscribe( - channels: (string | Actionable | ResolvedChannel)[], + channels: (string | Channel | ActionableChannel | ResolvedChannel)[], callback: (event: RealtimeResponseEvent) => void ): Promise; public async subscribe( - channelsOrChannel: string | Actionable | ResolvedChannel | (string | Actionable | ResolvedChannel)[], + channelsOrChannel: string | Channel | ActionableChannel | ResolvedChannel | (string | Channel | ActionableChannel | ResolvedChannel)[], callback: (event: RealtimeResponseEvent) => void ): Promise { const channelArray = Array.isArray(channelsOrChannel) From 9a74e33809f7f2891017b20918f12e2250f9ebd8 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 12 Jan 2026 21:02:10 +0530 Subject: [PATCH 22/26] refactor: enhance channel structure and type safety across multiple languages --- .../src/main/java/io/package/Channel.kt.twig | 290 ++++++++++----- .../java/io/package/services/Realtime.kt.twig | 35 +- templates/apple/Sources/Channel.swift.twig | 346 ++++++++++-------- .../Sources/Services/Realtime.swift.twig | 1 + templates/flutter/lib/channel.dart.twig | 226 ++++++------ templates/flutter/lib/src/realtime.dart.twig | 2 +- .../flutter/lib/src/realtime_base.dart.twig | 2 +- .../flutter/lib/src/realtime_mixin.dart.twig | 19 +- 8 files changed, 511 insertions(+), 410 deletions(-) diff --git a/templates/android/library/src/main/java/io/package/Channel.kt.twig b/templates/android/library/src/main/java/io/package/Channel.kt.twig index 133b04f23a..0fe97df1a0 100644 --- a/templates/android/library/src/main/java/io/package/Channel.kt.twig +++ b/templates/android/library/src/main/java/io/package/Channel.kt.twig @@ -1,13 +1,23 @@ package {{ sdk.namespace | caseDot }} -/** - * The result of building the channel. - */ -class ResolvedChannel(private val value: String) { - override fun toString(): String { - return value - } -} +// Marker interfaces for type safety +interface Root +interface Database +interface Collection +interface Document +interface TablesDB +interface Table +interface Row +interface Bucket +interface File +interface Func +interface Execution +interface Team +interface Membership +interface Resolved + +// Union type for actionable channels +typealias Actionable = Document /** * Helper for normalizing IDs @@ -18,126 +28,216 @@ private fun normalize(id: String): String { } /** - * Interface for actionable channels + * Channel class with generic type parameter for type-safe method chaining */ -interface Actionable { - val channel: String - fun create(): ResolvedChannel - fun update(): ResolvedChannel - fun delete(): ResolvedChannel -} +class Channel private constructor( + private val segments: List +) { + /** + * Internal helper to transition to next state with segment and ID + * Public for extension function access + */ + fun next(segment: String, id: String = "*"): Channel { + return Channel(segments + listOf(segment, normalize(id))) + } -interface Database { - fun collection(id: String = "*"): Collection -} + /** + * Internal helper for terminal actions (no ID segment) + * Public for extension function access + */ + fun resolve(action: String): Channel { + return Channel(segments + listOf(action)) + } -interface Collection { - fun document(id: String = "*"): Document -} + /** + * Convert channel to string representation + */ + override fun toString(): String = segments.joinToString(".") -interface Document : Actionable + companion object { + fun database(id: String = "*"): Channel = + Channel(listOf("databases", normalize(id))) -interface TablesDB { - fun table(id: String = "*"): Table -} + fun tablesdb(id: String = "*"): Channel = + Channel(listOf("tablesdb", normalize(id))) -interface Table { - fun row(id: String = "*"): Row -} + fun bucket(id: String = "*"): Channel = + Channel(listOf("buckets", normalize(id))) -interface Row : Actionable + fun function(id: String = "*"): Channel = + Channel(listOf("functions", normalize(id))) -interface Bucket { - fun file(id: String = "*"): File -} + fun team(id: String = "*"): Channel = + Channel(listOf("teams", normalize(id))) + + fun membership(id: String = "*"): Channel = + Channel(listOf("memberships", normalize(id))) -interface File : Actionable + fun account(userId: String = ""): String { + val id = normalize(userId) + return if (id == "*") "account" else "account.$id" + } -interface Function { - fun execution(id: String = "*"): Execution + // Global events + const val documents = "documents" + const val rows = "rows" + const val files = "files" + const val executions = "executions" + } } -interface Execution : Actionable +// --- DATABASE ROUTE --- +// Extension functions restricted by receiver type -private class ChannelBuilder( - private val segments: List -) : Database, Collection, Document, - TablesDB, Table, Row, - Bucket, File, - Function, Execution, Actionable { +/** + * Only available on Channel + */ +fun Channel.collection(id: String = "*"): Channel = + this.next("collections", id) - companion object { - fun start(segments: List) = ChannelBuilder(segments) - } +/** + * Only available on Channel + */ +fun Channel.document(id: String = "*"): Channel = + this.next("documents", id) - private fun next(segment: String, id: String): ChannelBuilder { - return ChannelBuilder(segments + listOf(segment, normalize(id))) - } +// --- TABLESDB ROUTE --- - override fun collection(id: String): Collection = - next("collections", id) +/** + * Only available on Channel + */ +fun Channel.table(id: String = "*"): Channel
= + this.next("tables", id) - override fun document(id: String): Document = - next("documents", id) +/** + * Only available on Channel
+ */ +fun Channel
.row(id: String = "*"): Channel = + this.next("rows", id) + +// --- BUCKET ROUTE --- + +/** + * Only available on Channel + */ +fun Channel.file(id: String = "*"): Channel = + this.next("files", id) - override fun table(id: String): Table = - next("tables", id) +// --- FUNCTION ROUTE --- - override fun row(id: String): Row = - next("rows", id) +/** + * Only available on Channel + */ +fun Channel.execution(id: String = "*"): Channel = + this.next("executions", id) + +// --- TERMINAL ACTIONS --- +// Restricted to Actionable types (Document, Row, File, Execution, Team, Membership) + +/** + * Only available on Channel + */ +fun Channel.create(): Channel = + this.resolve("create") - override fun file(id: String): File = - next("files", id) +/** + * Only available on Channel + */ +fun Channel.update(): Channel = + this.resolve("update") - override fun execution(id: String): Execution = - next("executions", id) +/** + * Only available on Channel + */ +fun Channel.delete(): Channel = + this.resolve("delete") - override val channel: String - get() = segments.joinToString(".") +/** + * Only available on Channel + */ +fun Channel.create(): Channel = + this.resolve("create") - override fun create(): ResolvedChannel = - ResolvedChannel("$channel.create") +/** + * Only available on Channel + */ +fun Channel.update(): Channel = + this.resolve("update") - override fun update(): ResolvedChannel = - ResolvedChannel("$channel.update") +/** + * Only available on Channel + */ +fun Channel.delete(): Channel = + this.resolve("delete") - override fun delete(): ResolvedChannel = - ResolvedChannel("$channel.delete") +/** + * Only available on Channel + */ +fun Channel.create(): Channel = + this.resolve("create") - override fun toString(): String = channel -} +/** + * Only available on Channel + */ +fun Channel.update(): Channel = + this.resolve("update") -class Channel private constructor() { +/** + * Only available on Channel + */ +fun Channel.delete(): Channel = + this.resolve("delete") - companion object { +/** + * Only available on Channel + */ +fun Channel.create(): Channel = + this.resolve("create") - fun database(id: String = "*"): Database = - ChannelBuilder.start(listOf("databases", normalize(id))) +/** + * Only available on Channel + */ +fun Channel.update(): Channel = + this.resolve("update") - fun tablesdb(id: String = "*"): TablesDB = - ChannelBuilder.start(listOf("tablesdb", normalize(id))) +/** + * Only available on Channel + */ +fun Channel.delete(): Channel = + this.resolve("delete") - fun bucket(id: String = "*"): Bucket = - ChannelBuilder.start(listOf("buckets", normalize(id))) +/** + * Only available on Channel + */ +fun Channel.create(): Channel = + this.resolve("create") - fun function(id: String = "*"): Function = - ChannelBuilder.start(listOf("functions", normalize(id))) +/** + * Only available on Channel + */ +fun Channel.update(): Channel = + this.resolve("update") - fun team(id: String = "*"): Actionable = - ChannelBuilder.start(listOf("teams", normalize(id))) +/** + * Only available on Channel + */ +fun Channel.delete(): Channel = + this.resolve("delete") - fun membership(id: String = "*"): Actionable = - ChannelBuilder.start(listOf("memberships", normalize(id))) +/** + * Only available on Channel + */ +fun Channel.create(): Channel = + this.resolve("create") - fun account(userId: String = ""): String { - val id = normalize(userId) - return if (id == "*") "account" else "account.$id" - } +/** + * Only available on Channel + */ +fun Channel.update(): Channel = + this.resolve("update") - // Global events - const val documents = "documents" - const val rows = "rows" - const val files = "files" - const val executions = "executions" - } -} +/** + * Only available on Channel + */ +fun Channel.delete(): Channel = + this.resolve("delete") diff --git a/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig b/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig index 4e0efea336..a6f500f2ef 100644 --- a/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig +++ b/templates/android/library/src/main/java/io/package/services/Realtime.kt.twig @@ -2,8 +2,7 @@ package {{ sdk.namespace | caseDot }}.services import {{ sdk.namespace | caseDot }}.Service import {{ sdk.namespace | caseDot }}.Client -import {{ sdk.namespace | caseDot }}.Actionable -import {{ sdk.namespace | caseDot }}.ResolvedChannel +import {{ sdk.namespace | caseDot }}.Channel import {{ sdk.namespace | caseDot }}.exceptions.{{ spec.title | caseUcfirst }}Exception import {{ sdk.namespace | caseDot }}.extensions.forEachAsync import {{ sdk.namespace | caseDot }}.extensions.fromJson @@ -113,30 +112,18 @@ class Realtime(client: Client) : Service(client), CoroutineScope { /** * Convert channel value to string - * Checks if it's an Actionable instance (use .channel), - * then ResolvedChannel (use .toString()), - * otherwise treats as string + * All Channel instances have toString() method */ private fun channelToString(channel: Any): String { return when { channel is String -> channel - channel is Actionable -> channel.channel - channel is ResolvedChannel -> channel.toString() + channel is Channel<*> -> channel.toString() else -> channel.toString() } } fun subscribe( - vararg channels: Actionable, - callback: (RealtimeResponseEvent) -> Unit, - ) = subscribe( - channels = channels.map { channelToString(it) }.toTypedArray(), - Any::class.java, - callback - ) - - fun subscribe( - vararg channels: ResolvedChannel, + vararg channels: Channel<*>, callback: (RealtimeResponseEvent) -> Unit, ) = subscribe( channels = channels.map { channelToString(it) }.toTypedArray(), @@ -154,19 +141,7 @@ class Realtime(client: Client) : Service(client), CoroutineScope { ) fun subscribe( - vararg channels: Actionable, - payloadType: Class, - callback: (RealtimeResponseEvent) -> Unit, - ): RealtimeSubscription { - return subscribe( - channels = channels.map { channelToString(it) }.toTypedArray(), - payloadType = payloadType, - callback = callback - ) - } - - fun subscribe( - vararg channels: ResolvedChannel, + vararg channels: Channel<*>, payloadType: Class, callback: (RealtimeResponseEvent) -> Unit, ): RealtimeSubscription { diff --git a/templates/apple/Sources/Channel.swift.twig b/templates/apple/Sources/Channel.swift.twig index 1420004be0..744b65afb7 100644 --- a/templates/apple/Sources/Channel.swift.twig +++ b/templates/apple/Sources/Channel.swift.twig @@ -1,31 +1,20 @@ import Foundation -/** - * Protocol for channel values that can be converted to strings - */ -public protocol ChannelValue { - func toString() -> String -} - -// Make String conform to ChannelValue -extension String: ChannelValue { - public func toString() -> String { - return self - } -} - -// The result of building the channel. -public final class ResolvedChannel { - private let value: String - - init(value: String) { - self.value = value - } - - public func toString() -> String { - value - } -} +// Marker structs for type safety (structs work with == constraints in extensions) +public struct Root {} +public struct Database {} +public struct Collection {} +public struct Document {} +public struct TablesDB {} +public struct Table {} +public struct Row {} +public struct Bucket {} +public struct File {} +public struct Func {} +public struct Execution {} +public struct Team {} +public struct Membership {} +public struct Resolved {} // Helper for normalizing IDs private func normalize(_ id: String) -> String { @@ -33,193 +22,228 @@ private func normalize(_ id: String) -> String { return trimmed.isEmpty ? "*" : trimmed } -// Protocol for actionable channels -public protocol Actionable: ChannelValue { - var channel: String { get } - func create() -> ResolvedChannel - func update() -> ResolvedChannel - func delete() -> ResolvedChannel -} - -// Provide default toString() implementation for Actionable -extension Actionable { +/// Channel class with generic type parameter for type-safe method chaining +public class RealtimeChannel { + private let segments: [String] + + internal init(_ segments: [String]) { + self.segments = segments + } + + /// Internal helper to transition to next state with segment and ID + internal func next(_ segment: String, _ id: String = "*") -> RealtimeChannel { + return RealtimeChannel(segments + [segment, normalize(id)]) + } + + /// Internal helper for terminal actions (no ID segment) + internal func resolve(_ action: String) -> RealtimeChannel { + return RealtimeChannel(segments + [action]) + } + + /// Convert channel to string representation public func toString() -> String { - return channel + return segments.joined(separator: ".") } } -public protocol IDatabase { - func collection(_ id: String) -> ICollection -} - -public protocol ICollection { - func document(_ id: String) -> IDocument -} - -public protocol IDocument: Actionable {} - -public protocol ITablesDB { - func table(_ id: String) -> ITable -} - -public protocol ITable { - func row(_ id: String) -> IRow +/// Non-generic Channel enum for static factory methods +public enum Channel { + + // MARK: - Root Factories + + public static func database(_ id: String = "*") -> RealtimeChannel { + return RealtimeChannel(["databases", normalize(id)]) + } + + public static func tablesdb(_ id: String = "*") -> RealtimeChannel { + return RealtimeChannel(["tablesdb", normalize(id)]) + } + + public static func bucket(_ id: String = "*") -> RealtimeChannel { + return RealtimeChannel(["buckets", normalize(id)]) + } + + public static func function(_ id: String = "*") -> RealtimeChannel { + return RealtimeChannel(["functions", normalize(id)]) + } + + public static func team(_ id: String = "*") -> RealtimeChannel { + return RealtimeChannel(["teams", normalize(id)]) + } + + public static func membership(_ id: String = "*") -> RealtimeChannel { + return RealtimeChannel(["memberships", normalize(id)]) + } + + public static func account(_ userId: String = "") -> String { + let id = normalize(userId) + return id == "*" ? "account" : "account.\(id)" + } + + // Global events + public static var documents: String { "documents" } + public static var rows: String { "rows" } + public static var files: String { "files" } + public static var executions: String { "executions" } } -public protocol IRow: Actionable {} +// MARK: - DATABASE ROUTE +// Protocol extensions restricted by receiver type -public protocol IBucket { - func file(_ id: String) -> IFile +/// Only available on RealtimeChannel +extension RealtimeChannel where T == Database { + public func collection(_ id: String = "*") -> RealtimeChannel { + return self.next("collections", id) + } } -public protocol IFile: Actionable {} - -public protocol IFunction { - func execution(_ id: String) -> IExecution +/// Only available on RealtimeChannel +extension RealtimeChannel where T == Collection { + public func document(_ id: String = "*") -> RealtimeChannel { + return self.next("documents", id) + } } -public protocol IExecution: Actionable {} +// MARK: - TABLESDB ROUTE -// Protocol extensions to provide default parameter overloads -// Swift doesn't allow default parameters in protocol requirements, -// so we provide overloaded methods without parameters -extension IDatabase { - public func collection() -> ICollection { - return collection("*") +/// Only available on RealtimeChannel +extension RealtimeChannel where T == TablesDB { + public func table(_ id: String = "*") -> RealtimeChannel
{ + return self.next("tables", id) } } -extension ICollection { - public func document() -> IDocument { - return document("*") +/// Only available on RealtimeChannel
+extension RealtimeChannel where T == Table { + public func row(_ id: String = "*") -> RealtimeChannel { + return self.next("rows", id) } } -extension ITablesDB { - public func table() -> ITable { - return table("*") - } -} +// MARK: - BUCKET ROUTE -extension ITable { - public func row() -> IRow { - return row("*") +/// Only available on RealtimeChannel +extension RealtimeChannel where T == Bucket { + public func file(_ id: String = "*") -> RealtimeChannel { + return self.next("files", id) } } -extension IBucket { - public func file() -> IFile { - return file("*") - } -} +// MARK: - FUNCTION ROUTE -extension IFunction { - public func execution() -> IExecution { - return execution("*") +/// Only available on RealtimeChannel +extension RealtimeChannel where T == Func { + public func execution(_ id: String = "*") -> RealtimeChannel { + return self.next("executions", id) } } -final class ChannelBuilder: - IDatabase, ICollection, IDocument, - ITablesDB, ITable, IRow, - IBucket, IFile, - IFunction, IExecution, Actionable -{ - private let segments: [String] +// MARK: - TERMINAL ACTIONS +// Restricted to actionable types (Document, Row, File, Execution, Team, Membership) - private init(_ segments: [String]) { - self.segments = segments +/// Only available on RealtimeChannel +extension RealtimeChannel where T == Document { + public func create() -> RealtimeChannel { + return self.resolve("create") } - - static func start(_ segments: [String]) -> ChannelBuilder { - ChannelBuilder(segments) + + public func update() -> RealtimeChannel { + return self.resolve("update") } - - private func next(_ segment: String, _ id: String) -> ChannelBuilder { - ChannelBuilder(segments + [segment, normalize(id)]) + + public func delete() -> RealtimeChannel { + return self.resolve("delete") } +} - // MARK: hierarchy - - func collection(_ id: String = "*") -> ICollection { - next("collections", id) +/// Only available on RealtimeChannel +extension RealtimeChannel where T == Row { + public func create() -> RealtimeChannel { + return self.resolve("create") } - - func document(_ id: String = "*") -> IDocument { - next("documents", id) + + public func update() -> RealtimeChannel { + return self.resolve("update") } - - func table(_ id: String = "*") -> ITable { - next("tables", id) + + public func delete() -> RealtimeChannel { + return self.resolve("delete") } +} - func row(_ id: String = "*") -> IRow { - next("rows", id) +/// Only available on RealtimeChannel +extension RealtimeChannel where T == File { + public func create() -> RealtimeChannel { + return self.resolve("create") } - - func file(_ id: String = "*") -> IFile { - next("files", id) + + public func update() -> RealtimeChannel { + return self.resolve("update") } - - func execution(_ id: String = "*") -> IExecution { - next("executions", id) + + public func delete() -> RealtimeChannel { + return self.resolve("delete") } +} - var channel: String { - segments.joined(separator: ".") +/// Only available on RealtimeChannel +extension RealtimeChannel where T == Execution { + public func create() -> RealtimeChannel { + return self.resolve("create") } - - func toString() -> String { - return channel + + public func update() -> RealtimeChannel { + return self.resolve("update") } - - func create() -> ResolvedChannel { - ResolvedChannel(value: "\(channel).create") + + public func delete() -> RealtimeChannel { + return self.resolve("delete") } +} - func update() -> ResolvedChannel { - ResolvedChannel(value: "\(channel).update") +/// Only available on RealtimeChannel +extension RealtimeChannel where T == Team { + public func create() -> RealtimeChannel { + return self.resolve("create") } - - func delete() -> ResolvedChannel { - ResolvedChannel(value: "\(channel).delete") + + public func update() -> RealtimeChannel { + return self.resolve("update") } -} - -public enum Channel { - - public static func database(_ id: String = "*") -> IDatabase { - ChannelBuilder.start(["databases", normalize(id)]) + + public func delete() -> RealtimeChannel { + return self.resolve("delete") } +} - public static func tablesdb(_ id: String = "*") -> ITablesDB { - ChannelBuilder.start(["tablesdb", normalize(id)]) +/// Only available on RealtimeChannel +extension RealtimeChannel where T == Membership { + public func create() -> RealtimeChannel { + return self.resolve("create") } - - public static func bucket(_ id: String = "*") -> IBucket { - ChannelBuilder.start(["buckets", normalize(id)]) + + public func update() -> RealtimeChannel { + return self.resolve("update") } - - public static func function(_ id: String = "*") -> IFunction { - ChannelBuilder.start(["functions", normalize(id)]) + + public func delete() -> RealtimeChannel { + return self.resolve("delete") } +} - public static func team(_ id: String = "*") -> Actionable { - ChannelBuilder.start(["teams", normalize(id)]) - } +// MARK: - Protocol for backward compatibility - public static func membership(_ id: String = "*") -> Actionable { - ChannelBuilder.start(["memberships", normalize(id)]) - } +/// Protocol for channel values that can be converted to strings +public protocol ChannelValue { + func toString() -> String +} - public static func account(_ userId: String = "") -> String { - let id = normalize(userId) - return id == "*" ? "account" : "account.\(id)" +// Make String conform to ChannelValue +extension String: ChannelValue { + public func toString() -> String { + return self } - - // Global events - public static let documents = "documents" - public static let rows = "rows" - public static let files = "files" - public static let executions = "executions" } + +// Make RealtimeChannel conform to ChannelValue +extension RealtimeChannel: ChannelValue {} diff --git a/templates/apple/Sources/Services/Realtime.swift.twig b/templates/apple/Sources/Services/Realtime.swift.twig index a588e7675f..992af98fba 100644 --- a/templates/apple/Sources/Services/Realtime.swift.twig +++ b/templates/apple/Sources/Services/Realtime.swift.twig @@ -119,6 +119,7 @@ open class Realtime : Service { /** * Convert channel value to string + * All Channel instances and String conform to ChannelValue */ private func channelToString(_ channel: ChannelValue) -> String { return channel.toString() diff --git a/templates/flutter/lib/channel.dart.twig b/templates/flutter/lib/channel.dart.twig index 077f89d9ff..ab7bb3dc1a 100644 --- a/templates/flutter/lib/channel.dart.twig +++ b/templates/flutter/lib/channel.dart.twig @@ -1,138 +1,154 @@ part of {{ language.params.packageName }}; -// The result of building the channel. -class ResolvedChannel { - final String value; - const ResolvedChannel(this.value); - - @override - String toString() => value; -} +// Marker classes for type safety +class Root {} +class Database {} +class Collection {} +class Document {} +class TablesDB {} +class Table {} +class Row {} +class Bucket {} +class File {} +class Func {} +class Execution {} +class Team {} +class Membership {} +class Resolved {} // Helper function for normalizing ID -String _normalize(String id) => id.trim().isEmpty ? '*' : id; +String _normalize(String id) => id.trim().isEmpty ? '*' : id.trim(); -// State interfaces +/// Channel class with generic type parameter for type-safe method chaining +class Channel { + final List _segments; -abstract class Actionable { - ResolvedChannel create(); - ResolvedChannel update(); - ResolvedChannel delete(); - String get channel; -} + Channel._(this._segments); -abstract class IDatabase { - ICollection collection([String id = '*']); -} + /// Internal helper to transition to next state with segment and ID + Channel _next(String segment, [String id = '*']) { + return Channel._([..._segments, segment, _normalize(id)]); + } -abstract class ICollection { - IDocument document([String id = '*']); -} + /// Internal helper for terminal actions (no ID segment) + Channel _resolve(String action) { + return Channel._([..._segments, action]); + } -abstract class IDocument implements Actionable {} + @override + String toString() => _segments.join('.'); -abstract class ITablesDB { - ITable table([String id = '*']); -} + // --- ROOT FACTORIES --- + static Channel database([String id = '*']) => + Channel._(['databases', _normalize(id)]); -abstract class ITable { - IRow row([String id = '*']); -} + static Channel tablesdb([String id = '*']) => + Channel._(['tablesdb', _normalize(id)]); -abstract class IRow implements Actionable {} + static Channel bucket([String id = '*']) => + Channel._(['buckets', _normalize(id)]); -abstract class IBucket { - IFile file([String id = '*']); + static Channel function([String id = '*']) => + Channel._(['functions', _normalize(id)]); + + static Channel team([String id = '*']) => + Channel._(['teams', _normalize(id)]); + + static Channel membership([String id = '*']) => + Channel._(['memberships', _normalize(id)]); + + static String account([String userId = '']) { + final id = _normalize(userId); + return id == '*' ? 'account' : 'account.$id'; + } + + // Global events + static String get documents => 'documents'; + static String get rows => 'rows'; + static String get files => 'files'; + static String get executions => 'executions'; } -abstract class IFile implements Actionable {} +// --- DATABASE ROUTE --- +// Extension methods restricted by receiver type -abstract class IFunction { - IExecution execution([String id = '*']); +/// Only available on Channel +extension DatabaseChannel on Channel { + Channel collection([String id = '*']) => _next('collections', id); } -abstract class IExecution implements Actionable {} +/// Only available on Channel +extension CollectionChannel on Channel { + Channel document([String id = '*']) => _next('documents', id); +} -// Builder implementation +// --- TABLESDB ROUTE --- -class _ChannelBuilder implements - IDatabase, ICollection, IDocument, - ITablesDB, ITable, IRow, - IBucket, IFile, - IFunction, IExecution, Actionable { - - final List _segments; +/// Only available on Channel +extension TablesDBChannel on Channel { + Channel
table([String id = '*']) => _next
('tables', id); +} - _ChannelBuilder._(this._segments); +/// Only available on Channel
+extension TableChannel on Channel
{ + Channel row([String id = '*']) => _next('rows', id); +} - _ChannelBuilder _next(String segment, String id) { - return _ChannelBuilder._([ - ..._segments, - segment, - _normalize(id), - ]); - } +// --- BUCKET ROUTE --- - @override - ICollection collection([String id = '*']) => _next('collections', id); - - @override - IDocument document([String id = '*']) => _next('documents', id); +/// Only available on Channel +extension BucketChannel on Channel { + Channel file([String id = '*']) => _next('files', id); +} - @override - ITable table([String id = '*']) => _next('tables', id); +// --- FUNCTION ROUTE --- - @override - IRow row([String id = '*']) => _next('rows', id); +/// Only available on Channel +extension FuncChannel on Channel { + Channel execution([String id = '*']) => _next('executions', id); +} - @override - IFile file([String id = '*']) => _next('files', id); +// --- TERMINAL ACTIONS --- +// Restricted to actionable types (Document, Row, File, Execution, Team, Membership) - @override - IExecution execution([String id = '*']) => _next('executions', id); +/// Only available on Channel +extension DocumentChannel on Channel { + Channel create() => _resolve('create'); + Channel update() => _resolve('update'); + Channel delete() => _resolve('delete'); +} - @override - ResolvedChannel create() => _resolve('create'); - @override - ResolvedChannel update() => _resolve('update'); - @override - ResolvedChannel delete() => _resolve('delete'); +/// Only available on Channel +extension RowChannel on Channel { + Channel create() => _resolve('create'); + Channel update() => _resolve('update'); + Channel delete() => _resolve('delete'); +} - @override - String get channel => _segments.join('.'); +/// Only available on Channel +extension FileChannel on Channel { + Channel create() => _resolve('create'); + Channel update() => _resolve('update'); + Channel delete() => _resolve('delete'); +} - @override - String toString() => channel; +/// Only available on Channel +extension ExecutionChannel on Channel { + Channel create() => _resolve('create'); + Channel update() => _resolve('update'); + Channel delete() => _resolve('delete'); +} - ResolvedChannel _resolve(String action) => ResolvedChannel('$channel.$action'); +/// Only available on Channel +extension TeamChannel on Channel { + Channel create() => _resolve('create'); + Channel update() => _resolve('update'); + Channel delete() => _resolve('delete'); } -// Entry point - -abstract class Channel { - static IDatabase database([String id = '*']) => - _ChannelBuilder._(['databases', _normalize(id)]); - - static ITablesDB tablesdb([String id = '*']) => - _ChannelBuilder._(['tablesdb', _normalize(id)]); - - static IBucket bucket([String id = '*']) => - _ChannelBuilder._(['buckets', _normalize(id)]); - - static IFunction function([String id = '*']) => - _ChannelBuilder._(['functions', _normalize(id)]); - - static Actionable team([String id = '*']) => - _ChannelBuilder._(['teams', _normalize(id)]); - - static Actionable membership([String id = '*']) => - _ChannelBuilder._(['memberships', _normalize(id)]); - - static String account([String userId = '']) => - userId.isEmpty ? 'account' : 'account.${_normalize(userId)}'; - // Global events (without ID) - static String get documents => 'documents'; - static String get rows => 'rows'; - static String get files => 'files'; - static String get executions => 'executions'; -} \ No newline at end of file +/// Only available on Channel +extension MembershipChannel on Channel { + Channel create() => _resolve('create'); + Channel update() => _resolve('update'); + Channel delete() => _resolve('delete'); +} diff --git a/templates/flutter/lib/src/realtime.dart.twig b/templates/flutter/lib/src/realtime.dart.twig index 14de266332..9b1f62e05d 100644 --- a/templates/flutter/lib/src/realtime.dart.twig +++ b/templates/flutter/lib/src/realtime.dart.twig @@ -50,7 +50,7 @@ abstract class Realtime extends Service { /// 'account.*' /// ]); /// ``` - RealtimeSubscription subscribe(List channels); // Object can be String, Actionable, or ResolvedChannel + RealtimeSubscription subscribe(List channels); // Object can be String or Channel /// The [close code](https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5) set when the WebSocket connection is closed. /// diff --git a/templates/flutter/lib/src/realtime_base.dart.twig b/templates/flutter/lib/src/realtime_base.dart.twig index 10c7a59098..0b48c6bfc6 100644 --- a/templates/flutter/lib/src/realtime_base.dart.twig +++ b/templates/flutter/lib/src/realtime_base.dart.twig @@ -3,5 +3,5 @@ import 'realtime.dart'; abstract class RealtimeBase implements Realtime { @override - RealtimeSubscription subscribe(List channels); // Object can be String, Actionable, or ResolvedChannel + RealtimeSubscription subscribe(List channels); // Object can be String or Channel } diff --git a/templates/flutter/lib/src/realtime_mixin.dart.twig b/templates/flutter/lib/src/realtime_mixin.dart.twig index 7a1557be4a..7919e97a17 100644 --- a/templates/flutter/lib/src/realtime_mixin.dart.twig +++ b/templates/flutter/lib/src/realtime_mixin.dart.twig @@ -169,24 +169,9 @@ mixin RealtimeMixin { } /// Convert channel value to string + /// Handles String and Channel instances (which have toString()) String _channelToString(Object channel) { - if (channel is String) { - return channel; - } - // Check if it's an Actionable instance (has channel getter) - // Use dynamic to avoid import conflicts with part file types - final dynamic ch = channel; - try { - // Try to access the channel property (Actionable interface) - final channelValue = ch.channel; - if (channelValue is String) { - return channelValue; - } - } catch (_) { - // Not an Actionable, fall through to toString() - } - // Check if it's a ResolvedChannel or any other type with toString() - return channel.toString(); + return channel is String ? channel : channel.toString(); } RealtimeSubscription subscribeTo(List channels) { From dde2c4b7c21401e32e038768310973968dddd35c Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 12 Jan 2026 21:09:56 +0530 Subject: [PATCH 23/26] refactor: update channel interfaces for improved type safety --- .../src/main/java/io/package/Channel.kt.twig | 142 ++++++++-------- templates/flutter/lib/channel.dart.twig | 154 +++++++++--------- 2 files changed, 148 insertions(+), 148 deletions(-) diff --git a/templates/android/library/src/main/java/io/package/Channel.kt.twig b/templates/android/library/src/main/java/io/package/Channel.kt.twig index 0fe97df1a0..baa07dec66 100644 --- a/templates/android/library/src/main/java/io/package/Channel.kt.twig +++ b/templates/android/library/src/main/java/io/package/Channel.kt.twig @@ -1,23 +1,23 @@ package {{ sdk.namespace | caseDot }} // Marker interfaces for type safety -interface Root -interface Database -interface Collection -interface Document -interface TablesDB -interface Table -interface Row -interface Bucket -interface File -interface Func -interface Execution -interface Team -interface Membership -interface Resolved +internal interface _Root +internal interface _Database +internal interface _Collection +internal interface _Document +internal interface _TablesDB +internal interface _Table +internal interface _Row +internal interface _Bucket +internal interface _File +internal interface _Func +internal interface _Execution +internal interface _Team +internal interface _Membership +internal interface _Resolved // Union type for actionable channels -typealias Actionable = Document +typealias Actionable = _Document /** * Helper for normalizing IDs @@ -45,7 +45,7 @@ class Channel private constructor( * Internal helper for terminal actions (no ID segment) * Public for extension function access */ - fun resolve(action: String): Channel { + fun resolve(action: String): Channel<_Resolved> { return Channel(segments + listOf(action)) } @@ -55,22 +55,22 @@ class Channel private constructor( override fun toString(): String = segments.joinToString(".") companion object { - fun database(id: String = "*"): Channel = + fun database(id: String = "*"): Channel<_Database> = Channel(listOf("databases", normalize(id))) - fun tablesdb(id: String = "*"): Channel = + fun tablesdb(id: String = "*"): Channel<_TablesDB> = Channel(listOf("tablesdb", normalize(id))) - fun bucket(id: String = "*"): Channel = + fun bucket(id: String = "*"): Channel<_Bucket> = Channel(listOf("buckets", normalize(id))) - fun function(id: String = "*"): Channel = + fun function(id: String = "*"): Channel<_Func> = Channel(listOf("functions", normalize(id))) - fun team(id: String = "*"): Channel = + fun team(id: String = "*"): Channel<_Team> = Channel(listOf("teams", normalize(id))) - fun membership(id: String = "*"): Channel = + fun membership(id: String = "*"): Channel<_Membership> = Channel(listOf("memberships", normalize(id))) fun account(userId: String = ""): String { @@ -90,154 +90,154 @@ class Channel private constructor( // Extension functions restricted by receiver type /** - * Only available on Channel + * Only available on Channel<_Database> */ -fun Channel.collection(id: String = "*"): Channel = +fun Channel<_Database>.collection(id: String = "*"): Channel<_Collection> = this.next("collections", id) /** - * Only available on Channel + * Only available on Channel<_Collection> */ -fun Channel.document(id: String = "*"): Channel = +fun Channel<_Collection>.document(id: String = "*"): Channel<_Document> = this.next("documents", id) // --- TABLESDB ROUTE --- /** - * Only available on Channel + * Only available on Channel<_TablesDB> */ -fun Channel.table(id: String = "*"): Channel
= +fun Channel<_TablesDB>.table(id: String = "*"): Channel<_Table> = this.next("tables", id) /** - * Only available on Channel
+ * Only available on Channel<_Table> */ -fun Channel
.row(id: String = "*"): Channel = +fun Channel<_Table>.row(id: String = "*"): Channel<_Row> = this.next("rows", id) // --- BUCKET ROUTE --- /** - * Only available on Channel + * Only available on Channel<_Bucket> */ -fun Channel.file(id: String = "*"): Channel = +fun Channel<_Bucket>.file(id: String = "*"): Channel<_File> = this.next("files", id) // --- FUNCTION ROUTE --- /** - * Only available on Channel + * Only available on Channel<_Func> */ -fun Channel.execution(id: String = "*"): Channel = +fun Channel<_Func>.execution(id: String = "*"): Channel<_Execution> = this.next("executions", id) // --- TERMINAL ACTIONS --- -// Restricted to Actionable types (Document, Row, File, Execution, Team, Membership) +// Restricted to Actionable types (_Document, _Row, _File, _Execution, _Team, _Membership) /** - * Only available on Channel + * Only available on Channel<_Document> */ -fun Channel.create(): Channel = +fun Channel<_Document>.create(): Channel<_Resolved> = this.resolve("create") /** - * Only available on Channel + * Only available on Channel<_Document> */ -fun Channel.update(): Channel = +fun Channel<_Document>.update(): Channel<_Resolved> = this.resolve("update") /** - * Only available on Channel + * Only available on Channel<_Document> */ -fun Channel.delete(): Channel = +fun Channel<_Document>.delete(): Channel<_Resolved> = this.resolve("delete") /** - * Only available on Channel + * Only available on Channel<_Row> */ -fun Channel.create(): Channel = +fun Channel<_Row>.create(): Channel<_Resolved> = this.resolve("create") /** - * Only available on Channel + * Only available on Channel<_Row> */ -fun Channel.update(): Channel = +fun Channel<_Row>.update(): Channel<_Resolved> = this.resolve("update") /** - * Only available on Channel + * Only available on Channel<_Row> */ -fun Channel.delete(): Channel = +fun Channel<_Row>.delete(): Channel<_Resolved> = this.resolve("delete") /** - * Only available on Channel + * Only available on Channel<_File> */ -fun Channel.create(): Channel = +fun Channel<_File>.create(): Channel<_Resolved> = this.resolve("create") /** - * Only available on Channel + * Only available on Channel<_File> */ -fun Channel.update(): Channel = +fun Channel<_File>.update(): Channel<_Resolved> = this.resolve("update") /** - * Only available on Channel + * Only available on Channel<_File> */ -fun Channel.delete(): Channel = +fun Channel<_File>.delete(): Channel<_Resolved> = this.resolve("delete") /** - * Only available on Channel + * Only available on Channel<_Execution> */ -fun Channel.create(): Channel = +fun Channel<_Execution>.create(): Channel<_Resolved> = this.resolve("create") /** - * Only available on Channel + * Only available on Channel<_Execution> */ -fun Channel.update(): Channel = +fun Channel<_Execution>.update(): Channel<_Resolved> = this.resolve("update") /** - * Only available on Channel + * Only available on Channel<_Execution> */ -fun Channel.delete(): Channel = +fun Channel<_Execution>.delete(): Channel<_Resolved> = this.resolve("delete") /** - * Only available on Channel + * Only available on Channel<_Team> */ -fun Channel.create(): Channel = +fun Channel<_Team>.create(): Channel<_Resolved> = this.resolve("create") /** - * Only available on Channel + * Only available on Channel<_Team> */ -fun Channel.update(): Channel = +fun Channel<_Team>.update(): Channel<_Resolved> = this.resolve("update") /** - * Only available on Channel + * Only available on Channel<_Team> */ -fun Channel.delete(): Channel = +fun Channel<_Team>.delete(): Channel<_Resolved> = this.resolve("delete") /** - * Only available on Channel + * Only available on Channel<_Membership> */ -fun Channel.create(): Channel = +fun Channel<_Membership>.create(): Channel<_Resolved> = this.resolve("create") /** - * Only available on Channel + * Only available on Channel<_Membership> */ -fun Channel.update(): Channel = +fun Channel<_Membership>.update(): Channel<_Resolved> = this.resolve("update") /** - * Only available on Channel + * Only available on Channel<_Membership> */ -fun Channel.delete(): Channel = +fun Channel<_Membership>.delete(): Channel<_Resolved> = this.resolve("delete") diff --git a/templates/flutter/lib/channel.dart.twig b/templates/flutter/lib/channel.dart.twig index ab7bb3dc1a..7f62ad6063 100644 --- a/templates/flutter/lib/channel.dart.twig +++ b/templates/flutter/lib/channel.dart.twig @@ -1,20 +1,20 @@ part of {{ language.params.packageName }}; // Marker classes for type safety -class Root {} -class Database {} -class Collection {} -class Document {} -class TablesDB {} -class Table {} -class Row {} -class Bucket {} -class File {} -class Func {} -class Execution {} -class Team {} -class Membership {} -class Resolved {} +class _Root {} +class _Database {} +class _Collection {} +class _Document {} +class _TablesDB {} +class _Table {} +class _Row {} +class _Bucket {} +class _File {} +class _Func {} +class _Execution {} +class _Team {} +class _Membership {} +class _Resolved {} // Helper function for normalizing ID String _normalize(String id) => id.trim().isEmpty ? '*' : id.trim(); @@ -31,31 +31,31 @@ class Channel { } /// Internal helper for terminal actions (no ID segment) - Channel _resolve(String action) { - return Channel._([..._segments, action]); + Channel<_Resolved> _resolve(String action) { + return Channel<_Resolved>._([..._segments, action]); } @override String toString() => _segments.join('.'); // --- ROOT FACTORIES --- - static Channel database([String id = '*']) => - Channel._(['databases', _normalize(id)]); + static Channel<_Database> database([String id = '*']) => + Channel<_Database>._(['databases', _normalize(id)]); - static Channel tablesdb([String id = '*']) => - Channel._(['tablesdb', _normalize(id)]); + static Channel<_TablesDB> tablesdb([String id = '*']) => + Channel<_TablesDB>._(['tablesdb', _normalize(id)]); - static Channel bucket([String id = '*']) => - Channel._(['buckets', _normalize(id)]); + static Channel<_Bucket> bucket([String id = '*']) => + Channel<_Bucket>._(['buckets', _normalize(id)]); - static Channel function([String id = '*']) => - Channel._(['functions', _normalize(id)]); + static Channel<_Func> function([String id = '*']) => + Channel<_Func>._(['functions', _normalize(id)]); - static Channel team([String id = '*']) => - Channel._(['teams', _normalize(id)]); + static Channel<_Team> team([String id = '*']) => + Channel<_Team>._(['teams', _normalize(id)]); - static Channel membership([String id = '*']) => - Channel._(['memberships', _normalize(id)]); + static Channel<_Membership> membership([String id = '*']) => + Channel<_Membership>._(['memberships', _normalize(id)]); static String account([String userId = '']) { final id = _normalize(userId); @@ -72,83 +72,83 @@ class Channel { // --- DATABASE ROUTE --- // Extension methods restricted by receiver type -/// Only available on Channel -extension DatabaseChannel on Channel { - Channel collection([String id = '*']) => _next('collections', id); +/// Only available on Channel<_Database> +extension DatabaseChannel on Channel<_Database> { + Channel<_Collection> collection([String id = '*']) => _next<_Collection>('collections', id); } -/// Only available on Channel -extension CollectionChannel on Channel { - Channel document([String id = '*']) => _next('documents', id); +/// Only available on Channel<_Collection> +extension CollectionChannel on Channel<_Collection> { + Channel<_Document> document([String id = '*']) => _next<_Document>('documents', id); } // --- TABLESDB ROUTE --- -/// Only available on Channel -extension TablesDBChannel on Channel { - Channel
table([String id = '*']) => _next
('tables', id); +/// Only available on Channel<_TablesDB> +extension TablesDBChannel on Channel<_TablesDB> { + Channel<_Table> table([String id = '*']) => _next<_Table>('tables', id); } -/// Only available on Channel
-extension TableChannel on Channel
{ - Channel row([String id = '*']) => _next('rows', id); +/// Only available on Channel<_Table> +extension TableChannel on Channel<_Table> { + Channel<_Row> row([String id = '*']) => _next<_Row>('rows', id); } // --- BUCKET ROUTE --- -/// Only available on Channel -extension BucketChannel on Channel { - Channel file([String id = '*']) => _next('files', id); +/// Only available on Channel<_Bucket> +extension BucketChannel on Channel<_Bucket> { + Channel<_File> file([String id = '*']) => _next<_File>('files', id); } // --- FUNCTION ROUTE --- -/// Only available on Channel -extension FuncChannel on Channel { - Channel execution([String id = '*']) => _next('executions', id); +/// Only available on Channel<_Func> +extension FuncChannel on Channel<_Func> { + Channel<_Execution> execution([String id = '*']) => _next<_Execution>('executions', id); } // --- TERMINAL ACTIONS --- -// Restricted to actionable types (Document, Row, File, Execution, Team, Membership) +// Restricted to actionable types (_Document, _Row, _File, _Execution, _Team, _Membership) -/// Only available on Channel -extension DocumentChannel on Channel { - Channel create() => _resolve('create'); - Channel update() => _resolve('update'); - Channel delete() => _resolve('delete'); +/// Only available on Channel<_Document> +extension DocumentChannel on Channel<_Document> { + Channel<_Resolved> create() => _resolve('create'); + Channel<_Resolved> update() => _resolve('update'); + Channel<_Resolved> delete() => _resolve('delete'); } -/// Only available on Channel -extension RowChannel on Channel { - Channel create() => _resolve('create'); - Channel update() => _resolve('update'); - Channel delete() => _resolve('delete'); +/// Only available on Channel<_Row> +extension RowChannel on Channel<_Row> { + Channel<_Resolved> create() => _resolve('create'); + Channel<_Resolved> update() => _resolve('update'); + Channel<_Resolved> delete() => _resolve('delete'); } -/// Only available on Channel -extension FileChannel on Channel { - Channel create() => _resolve('create'); - Channel update() => _resolve('update'); - Channel delete() => _resolve('delete'); +/// Only available on Channel<_File> +extension FileChannel on Channel<_File> { + Channel<_Resolved> create() => _resolve('create'); + Channel<_Resolved> update() => _resolve('update'); + Channel<_Resolved> delete() => _resolve('delete'); } -/// Only available on Channel -extension ExecutionChannel on Channel { - Channel create() => _resolve('create'); - Channel update() => _resolve('update'); - Channel delete() => _resolve('delete'); +/// Only available on Channel<_Execution> +extension ExecutionChannel on Channel<_Execution> { + Channel<_Resolved> create() => _resolve('create'); + Channel<_Resolved> update() => _resolve('update'); + Channel<_Resolved> delete() => _resolve('delete'); } -/// Only available on Channel -extension TeamChannel on Channel { - Channel create() => _resolve('create'); - Channel update() => _resolve('update'); - Channel delete() => _resolve('delete'); +/// Only available on Channel<_Team> +extension TeamChannel on Channel<_Team> { + Channel<_Resolved> create() => _resolve('create'); + Channel<_Resolved> update() => _resolve('update'); + Channel<_Resolved> delete() => _resolve('delete'); } -/// Only available on Channel -extension MembershipChannel on Channel { - Channel create() => _resolve('create'); - Channel update() => _resolve('update'); - Channel delete() => _resolve('delete'); +/// Only available on Channel<_Membership> +extension MembershipChannel on Channel<_Membership> { + Channel<_Resolved> create() => _resolve('create'); + Channel<_Resolved> update() => _resolve('update'); + Channel<_Resolved> delete() => _resolve('delete'); } From 3ed2d3e52e9bc258e2edd36cbc37188ba931434e Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 12 Jan 2026 21:23:42 +0530 Subject: [PATCH 24/26] updated kotlin --- .../src/main/java/io/package/Channel.kt.twig | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/templates/android/library/src/main/java/io/package/Channel.kt.twig b/templates/android/library/src/main/java/io/package/Channel.kt.twig index baa07dec66..c9b8cb1c6d 100644 --- a/templates/android/library/src/main/java/io/package/Channel.kt.twig +++ b/templates/android/library/src/main/java/io/package/Channel.kt.twig @@ -1,20 +1,20 @@ package {{ sdk.namespace | caseDot }} // Marker interfaces for type safety -internal interface _Root -internal interface _Database -internal interface _Collection -internal interface _Document -internal interface _TablesDB -internal interface _Table -internal interface _Row -internal interface _Bucket -internal interface _File -internal interface _Func -internal interface _Execution -internal interface _Team -internal interface _Membership -internal interface _Resolved +public interface _Root +public interface _Database +public interface _Collection +public interface _Document +public interface _TablesDB +public interface _Table +public interface _Row +public interface _Bucket +public interface _File +public interface _Func +public interface _Execution +public interface _Team +public interface _Membership +public interface _Resolved // Union type for actionable channels typealias Actionable = _Document From 163e7fc9d04b1c247210801599e1e39926c954ec Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Mon, 12 Jan 2026 21:25:31 +0530 Subject: [PATCH 25/26] updated swift issue --- templates/apple/Sources/Channel.swift.twig | 154 ++++++++++----------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/templates/apple/Sources/Channel.swift.twig b/templates/apple/Sources/Channel.swift.twig index 744b65afb7..f81d8dc5b1 100644 --- a/templates/apple/Sources/Channel.swift.twig +++ b/templates/apple/Sources/Channel.swift.twig @@ -1,20 +1,20 @@ import Foundation // Marker structs for type safety (structs work with == constraints in extensions) -public struct Root {} -public struct Database {} -public struct Collection {} -public struct Document {} -public struct TablesDB {} -public struct Table {} -public struct Row {} -public struct Bucket {} -public struct File {} -public struct Func {} -public struct Execution {} -public struct Team {} -public struct Membership {} -public struct Resolved {} +public struct _Root {} +public struct _Database {} +public struct _Collection {} +public struct _Document {} +public struct _TablesDB {} +public struct _Table {} +public struct _Row {} +public struct _Bucket {} +public struct _File {} +public struct _Func {} +public struct _Execution {} +public struct _Team {} +public struct _Membership {} +public struct _Resolved {} // Helper for normalizing IDs private func normalize(_ id: String) -> String { @@ -36,8 +36,8 @@ public class RealtimeChannel { } /// Internal helper for terminal actions (no ID segment) - internal func resolve(_ action: String) -> RealtimeChannel { - return RealtimeChannel(segments + [action]) + internal func resolve(_ action: String) -> RealtimeChannel<_Resolved> { + return RealtimeChannel<_Resolved>(segments + [action]) } /// Convert channel to string representation @@ -51,28 +51,28 @@ public enum Channel { // MARK: - Root Factories - public static func database(_ id: String = "*") -> RealtimeChannel { - return RealtimeChannel(["databases", normalize(id)]) + public static func database(_ id: String = "*") -> RealtimeChannel<_Database> { + return RealtimeChannel<_Database>(["databases", normalize(id)]) } - public static func tablesdb(_ id: String = "*") -> RealtimeChannel { - return RealtimeChannel(["tablesdb", normalize(id)]) + public static func tablesdb(_ id: String = "*") -> RealtimeChannel<_TablesDB> { + return RealtimeChannel<_TablesDB>(["tablesdb", normalize(id)]) } - public static func bucket(_ id: String = "*") -> RealtimeChannel { - return RealtimeChannel(["buckets", normalize(id)]) + public static func bucket(_ id: String = "*") -> RealtimeChannel<_Bucket> { + return RealtimeChannel<_Bucket>(["buckets", normalize(id)]) } - public static func function(_ id: String = "*") -> RealtimeChannel { - return RealtimeChannel(["functions", normalize(id)]) + public static func function(_ id: String = "*") -> RealtimeChannel<_Func> { + return RealtimeChannel<_Func>(["functions", normalize(id)]) } - public static func team(_ id: String = "*") -> RealtimeChannel { - return RealtimeChannel(["teams", normalize(id)]) + public static func team(_ id: String = "*") -> RealtimeChannel<_Team> { + return RealtimeChannel<_Team>(["teams", normalize(id)]) } - public static func membership(_ id: String = "*") -> RealtimeChannel { - return RealtimeChannel(["memberships", normalize(id)]) + public static func membership(_ id: String = "*") -> RealtimeChannel<_Membership> { + return RealtimeChannel<_Membership>(["memberships", normalize(id)]) } public static func account(_ userId: String = "") -> String { @@ -90,143 +90,143 @@ public enum Channel { // MARK: - DATABASE ROUTE // Protocol extensions restricted by receiver type -/// Only available on RealtimeChannel -extension RealtimeChannel where T == Database { - public func collection(_ id: String = "*") -> RealtimeChannel { +/// Only available on RealtimeChannel<_Database> +extension RealtimeChannel where T == _Database { + public func collection(_ id: String = "*") -> RealtimeChannel<_Collection> { return self.next("collections", id) } } -/// Only available on RealtimeChannel -extension RealtimeChannel where T == Collection { - public func document(_ id: String = "*") -> RealtimeChannel { +/// Only available on RealtimeChannel<_Collection> +extension RealtimeChannel where T == _Collection { + public func document(_ id: String = "*") -> RealtimeChannel<_Document> { return self.next("documents", id) } } // MARK: - TABLESDB ROUTE -/// Only available on RealtimeChannel -extension RealtimeChannel where T == TablesDB { - public func table(_ id: String = "*") -> RealtimeChannel
{ +/// Only available on RealtimeChannel<_TablesDB> +extension RealtimeChannel where T == _TablesDB { + public func table(_ id: String = "*") -> RealtimeChannel<_Table> { return self.next("tables", id) } } -/// Only available on RealtimeChannel
-extension RealtimeChannel where T == Table { - public func row(_ id: String = "*") -> RealtimeChannel { +/// Only available on RealtimeChannel<_Table> +extension RealtimeChannel where T == _Table { + public func row(_ id: String = "*") -> RealtimeChannel<_Row> { return self.next("rows", id) } } // MARK: - BUCKET ROUTE -/// Only available on RealtimeChannel -extension RealtimeChannel where T == Bucket { - public func file(_ id: String = "*") -> RealtimeChannel { +/// Only available on RealtimeChannel<_Bucket> +extension RealtimeChannel where T == _Bucket { + public func file(_ id: String = "*") -> RealtimeChannel<_File> { return self.next("files", id) } } // MARK: - FUNCTION ROUTE -/// Only available on RealtimeChannel -extension RealtimeChannel where T == Func { - public func execution(_ id: String = "*") -> RealtimeChannel { +/// Only available on RealtimeChannel<_Func> +extension RealtimeChannel where T == _Func { + public func execution(_ id: String = "*") -> RealtimeChannel<_Execution> { return self.next("executions", id) } } // MARK: - TERMINAL ACTIONS -// Restricted to actionable types (Document, Row, File, Execution, Team, Membership) +// Restricted to actionable types (_Document, _Row, _File, _Execution, _Team, _Membership) -/// Only available on RealtimeChannel -extension RealtimeChannel where T == Document { - public func create() -> RealtimeChannel { +/// Only available on RealtimeChannel<_Document> +extension RealtimeChannel where T == _Document { + public func create() -> RealtimeChannel<_Resolved> { return self.resolve("create") } - public func update() -> RealtimeChannel { + public func update() -> RealtimeChannel<_Resolved> { return self.resolve("update") } - public func delete() -> RealtimeChannel { + public func delete() -> RealtimeChannel<_Resolved> { return self.resolve("delete") } } -/// Only available on RealtimeChannel -extension RealtimeChannel where T == Row { - public func create() -> RealtimeChannel { +/// Only available on RealtimeChannel<_Row> +extension RealtimeChannel where T == _Row { + public func create() -> RealtimeChannel<_Resolved> { return self.resolve("create") } - public func update() -> RealtimeChannel { + public func update() -> RealtimeChannel<_Resolved> { return self.resolve("update") } - public func delete() -> RealtimeChannel { + public func delete() -> RealtimeChannel<_Resolved> { return self.resolve("delete") } } -/// Only available on RealtimeChannel -extension RealtimeChannel where T == File { - public func create() -> RealtimeChannel { +/// Only available on RealtimeChannel<_File> +extension RealtimeChannel where T == _File { + public func create() -> RealtimeChannel<_Resolved> { return self.resolve("create") } - public func update() -> RealtimeChannel { + public func update() -> RealtimeChannel<_Resolved> { return self.resolve("update") } - public func delete() -> RealtimeChannel { + public func delete() -> RealtimeChannel<_Resolved> { return self.resolve("delete") } } -/// Only available on RealtimeChannel -extension RealtimeChannel where T == Execution { - public func create() -> RealtimeChannel { +/// Only available on RealtimeChannel<_Execution> +extension RealtimeChannel where T == _Execution { + public func create() -> RealtimeChannel<_Resolved> { return self.resolve("create") } - public func update() -> RealtimeChannel { + public func update() -> RealtimeChannel<_Resolved> { return self.resolve("update") } - public func delete() -> RealtimeChannel { + public func delete() -> RealtimeChannel<_Resolved> { return self.resolve("delete") } } -/// Only available on RealtimeChannel -extension RealtimeChannel where T == Team { - public func create() -> RealtimeChannel { +/// Only available on RealtimeChannel<_Team> +extension RealtimeChannel where T == _Team { + public func create() -> RealtimeChannel<_Resolved> { return self.resolve("create") } - public func update() -> RealtimeChannel { + public func update() -> RealtimeChannel<_Resolved> { return self.resolve("update") } - public func delete() -> RealtimeChannel { + public func delete() -> RealtimeChannel<_Resolved> { return self.resolve("delete") } } -/// Only available on RealtimeChannel -extension RealtimeChannel where T == Membership { - public func create() -> RealtimeChannel { +/// Only available on RealtimeChannel<_Membership> +extension RealtimeChannel where T == _Membership { + public func create() -> RealtimeChannel<_Resolved> { return self.resolve("create") } - public func update() -> RealtimeChannel { + public func update() -> RealtimeChannel<_Resolved> { return self.resolve("update") } - public func delete() -> RealtimeChannel { + public func delete() -> RealtimeChannel<_Resolved> { return self.resolve("delete") } } From 8af59f4eaf90cd32af5b6463db607d543260b173 Mon Sep 17 00:00:00 2001 From: ArnabChatterjee20k Date: Tue, 13 Jan 2026 11:52:30 +0530 Subject: [PATCH 26/26] fixed android client build --- .../src/main/java/io/package/Channel.kt.twig | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/templates/android/library/src/main/java/io/package/Channel.kt.twig b/templates/android/library/src/main/java/io/package/Channel.kt.twig index c9b8cb1c6d..94b3426cbf 100644 --- a/templates/android/library/src/main/java/io/package/Channel.kt.twig +++ b/templates/android/library/src/main/java/io/package/Channel.kt.twig @@ -137,107 +137,125 @@ fun Channel<_Func>.execution(id: String = "*"): Channel<_Execution> = /** * Only available on Channel<_Document> */ +@JvmName("createDocument") fun Channel<_Document>.create(): Channel<_Resolved> = this.resolve("create") /** * Only available on Channel<_Document> */ +@JvmName("updateDocument") fun Channel<_Document>.update(): Channel<_Resolved> = this.resolve("update") /** * Only available on Channel<_Document> */ +@JvmName("deleteDocument") fun Channel<_Document>.delete(): Channel<_Resolved> = this.resolve("delete") /** * Only available on Channel<_Row> */ +@JvmName("createRow") fun Channel<_Row>.create(): Channel<_Resolved> = this.resolve("create") /** * Only available on Channel<_Row> */ +@JvmName("updateRow") fun Channel<_Row>.update(): Channel<_Resolved> = this.resolve("update") /** * Only available on Channel<_Row> */ +@JvmName("deleteRow") fun Channel<_Row>.delete(): Channel<_Resolved> = this.resolve("delete") /** * Only available on Channel<_File> */ +@JvmName("createFile") fun Channel<_File>.create(): Channel<_Resolved> = this.resolve("create") /** * Only available on Channel<_File> */ +@JvmName("updateFile") fun Channel<_File>.update(): Channel<_Resolved> = this.resolve("update") /** * Only available on Channel<_File> */ +@JvmName("deleteFile") fun Channel<_File>.delete(): Channel<_Resolved> = this.resolve("delete") /** * Only available on Channel<_Execution> */ +@JvmName("createExecution") fun Channel<_Execution>.create(): Channel<_Resolved> = this.resolve("create") /** * Only available on Channel<_Execution> */ +@JvmName("updateExecution") fun Channel<_Execution>.update(): Channel<_Resolved> = this.resolve("update") /** * Only available on Channel<_Execution> */ +@JvmName("deleteExecution") fun Channel<_Execution>.delete(): Channel<_Resolved> = this.resolve("delete") /** * Only available on Channel<_Team> */ +@JvmName("createTeam") fun Channel<_Team>.create(): Channel<_Resolved> = this.resolve("create") /** * Only available on Channel<_Team> */ +@JvmName("updateTeam") fun Channel<_Team>.update(): Channel<_Resolved> = this.resolve("update") /** * Only available on Channel<_Team> */ +@JvmName("deleteTeam") fun Channel<_Team>.delete(): Channel<_Resolved> = this.resolve("delete") /** * Only available on Channel<_Membership> */ +@JvmName("createMembership") fun Channel<_Membership>.create(): Channel<_Resolved> = this.resolve("create") /** * Only available on Channel<_Membership> */ +@JvmName("updateMembership") fun Channel<_Membership>.update(): Channel<_Resolved> = this.resolve("update") /** * Only available on Channel<_Membership> */ +@JvmName("deleteMembership") fun Channel<_Membership>.delete(): Channel<_Resolved> = this.resolve("delete")