diff --git a/Sources/Realtime/RealtimeChannel+AsyncAwait.swift b/Sources/Realtime/RealtimeChannel+AsyncAwait.swift index c8a51c3e..8a12a4d9 100644 --- a/Sources/Realtime/RealtimeChannel+AsyncAwait.swift +++ b/Sources/Realtime/RealtimeChannel+AsyncAwait.swift @@ -24,6 +24,23 @@ extension RealtimeChannelV2 { } /// Listen for postgres changes in a channel. + public func postgresChange( + _: InsertAction.Type, + schema: String = "public", + table: String? = nil, + filter: RealtimePostgresFilter? = nil + ) -> AsyncStream { + postgresChange(event: .insert, schema: schema, table: table, filter: filter?.value) + .compactErase() + } + + /// Listen for postgres changes in a channel. + @available( + *, + deprecated, + message: "Use the new filter syntax instead." + ) + @_disfavoredOverload public func postgresChange( _: InsertAction.Type, schema: String = "public", @@ -35,6 +52,23 @@ extension RealtimeChannelV2 { } /// Listen for postgres changes in a channel. + public func postgresChange( + _: UpdateAction.Type, + schema: String = "public", + table: String? = nil, + filter: RealtimePostgresFilter? = nil + ) -> AsyncStream { + postgresChange(event: .update, schema: schema, table: table, filter: filter?.value) + .compactErase() + } + + /// Listen for postgres changes in a channel. + @available( + *, + deprecated, + message: "Use the new filter syntax instead." + ) + @_disfavoredOverload public func postgresChange( _: UpdateAction.Type, schema: String = "public", @@ -46,6 +80,23 @@ extension RealtimeChannelV2 { } /// Listen for postgres changes in a channel. + public func postgresChange( + _: DeleteAction.Type, + schema: String = "public", + table: String? = nil, + filter: RealtimePostgresFilter? = nil + ) -> AsyncStream { + postgresChange(event: .delete, schema: schema, table: table, filter: filter?.value) + .compactErase() + } + + /// Listen for postgres changes in a channel. + @available( + *, + deprecated, + message: "Use the new filter syntax instead." + ) + @_disfavoredOverload public func postgresChange( _: DeleteAction.Type, schema: String = "public", @@ -57,6 +108,22 @@ extension RealtimeChannelV2 { } /// Listen for postgres changes in a channel. + public func postgresChange( + _: AnyAction.Type, + schema: String = "public", + table: String? = nil, + filter: RealtimePostgresFilter? = nil + ) -> AsyncStream { + postgresChange(event: .all, schema: schema, table: table, filter: filter?.value) + } + + /// Listen for postgres changes in a channel. + @available( + *, + deprecated, + message: "Use the new filter syntax instead." + ) + @_disfavoredOverload public func postgresChange( _: AnyAction.Type, schema: String = "public", diff --git a/Sources/Realtime/RealtimePostgresFilter.swift b/Sources/Realtime/RealtimePostgresFilter.swift new file mode 100644 index 00000000..8b8c7790 --- /dev/null +++ b/Sources/Realtime/RealtimePostgresFilter.swift @@ -0,0 +1,36 @@ +// +// RealtimePostgresFilter.swift +// Supabase +// +// Created by Lucas Abijmil on 19/02/2025. +// + +/// A filter that can be used in Realtime. +public enum RealtimePostgresFilter { + case eq(_ column: String, value: any RealtimePostgresFilterValue) + case neq(_ column: String, value: any RealtimePostgresFilterValue) + case gt(_ column: String, value: any RealtimePostgresFilterValue) + case gte(_ column: String, value: any RealtimePostgresFilterValue) + case lt(_ column: String, value: any RealtimePostgresFilterValue) + case lte(_ column: String, value: any RealtimePostgresFilterValue) + case `in`(_ column: String, values: [any RealtimePostgresFilterValue]) + + var value: String { + switch self { + case let .eq(column, value): + return "\(column)=eq.\(value.rawValue)" + case let .neq(column, value): + return "\(column)=neq.\(value.rawValue)" + case let .gt(column, value): + return "\(column)=gt.\(value.rawValue)" + case let .gte(column, value): + return "\(column)=gte.\(value.rawValue)" + case let .lt(column, value): + return "\(column)=lt.\(value.rawValue)" + case let .lte(column, value): + return "\(column)=lte.\(value.rawValue)" + case let .in(column, values): + return "\(column)=in.(\(values.map(\.rawValue).joined(separator: ",")))" + } + } +} diff --git a/Sources/Realtime/RealtimePostgresFilterValue.swift b/Sources/Realtime/RealtimePostgresFilterValue.swift new file mode 100644 index 00000000..259e4c72 --- /dev/null +++ b/Sources/Realtime/RealtimePostgresFilterValue.swift @@ -0,0 +1,41 @@ +// +// RealtimePostgresFilterValue.swift +// Supabase +// +// Created by Lucas Abijmil on 19/02/2025. +// + +import Foundation + +/// A value that can be used to filter Realtime changes in a channel. +public protocol RealtimePostgresFilterValue { + var rawValue: String { get } +} + +extension String: RealtimePostgresFilterValue { + public var rawValue: String { self } +} + +extension Int: RealtimePostgresFilterValue { + public var rawValue: String { "\(self)" } +} + +extension Double: RealtimePostgresFilterValue { + public var rawValue: String { "\(self)" } +} + +extension Bool: RealtimePostgresFilterValue { + public var rawValue: String { "\(self)" } +} + +extension UUID: RealtimePostgresFilterValue { + public var rawValue: String { uuidString } +} + +extension Date: RealtimePostgresFilterValue { + public var rawValue: String { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter.string(from: self) + } +} diff --git a/Tests/RealtimeTests/RealtimePostgresFilterTests.swift b/Tests/RealtimeTests/RealtimePostgresFilterTests.swift new file mode 100644 index 00000000..78b5e8e9 --- /dev/null +++ b/Tests/RealtimeTests/RealtimePostgresFilterTests.swift @@ -0,0 +1,68 @@ +// +// RealtimePostgresFilterTests.swift +// Supabase +// +// Created by Lucas Abijmil on 20/02/2025. +// + +import XCTest +@testable import Realtime + +final class RealtimePostgresFilterTests: XCTestCase { + + func testEq() { + let value = "value" + let column = "column" + let filter: RealtimePostgresFilter = .eq(column, value: value) + + XCTAssertEqual(filter.value, "column=eq.value") + } + + func testNeq() { + let value = "value" + let column = "column" + let filter: RealtimePostgresFilter = .neq(column, value: value) + + XCTAssertEqual(filter.value, "column=neq.value") + } + + func testGt() { + let value = "value" + let column = "column" + let filter: RealtimePostgresFilter = .gt(column, value: value) + + XCTAssertEqual(filter.value, "column=gt.value") + } + + func testGte() { + let value = "value" + let column = "column" + let filter: RealtimePostgresFilter = .gte(column, value: value) + + XCTAssertEqual(filter.value, "column=gte.value") + } + + func testLt() { + let value = "value" + let column = "column" + let filter: RealtimePostgresFilter = .lt(column, value: value) + + XCTAssertEqual(filter.value, "column=lt.value") + } + + func testLte() { + let value = "value" + let column = "column" + let filter: RealtimePostgresFilter = .lte(column, value: value) + + XCTAssertEqual(filter.value, "column=lte.value") + } + + func testIn() { + let values = ["value1", "value2"] + let column = "column" + let filter: RealtimePostgresFilter = .in(column, values: values) + + XCTAssertEqual(filter.value, "column=in.(value1,value2)") + } +} diff --git a/Tests/RealtimeTests/RealtimePostgresFilterValueTests.swift b/Tests/RealtimeTests/RealtimePostgresFilterValueTests.swift new file mode 100644 index 00000000..a931cb1f --- /dev/null +++ b/Tests/RealtimeTests/RealtimePostgresFilterValueTests.swift @@ -0,0 +1,24 @@ +// +// RealtimePostgresFilterValueTests.swift +// Supabase +// +// Created by Lucas Abijmil on 19/02/2025. +// + +import XCTest +@testable import Realtime + +final class RealtimePostgresFilterValueTests: XCTestCase { + func testUUID() { + XCTAssertEqual( + UUID(uuidString: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F")!.rawValue, + "E621E1F8-C36C-495A-93FC-0C247A3E6E5F") + } + + func testDate() { + XCTAssertEqual( + Date(timeIntervalSince1970: 1_737_465_985).rawValue, + "2025-01-21T13:26:25.000Z" + ) + } +}