diff --git a/.swiftformat b/.swiftformat index fc594a28..9328c32f 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,4 +1,4 @@ ---swiftversion 5.7 +--swiftversion 5.9 --binarygrouping none --decimalgrouping none --hexgrouping none @@ -9,4 +9,3 @@ --wrapcollections before-first --wrapparameters before-first --extensionacl on-declarations ---maxwidth 100 diff --git a/Examples/Examples/AnyJSONView.swift b/Examples/Examples/AnyJSONView.swift index 978f9c87..97b7f8a7 100644 --- a/Examples/Examples/AnyJSONView.swift +++ b/Examples/Examples/AnyJSONView.swift @@ -58,9 +58,9 @@ extension AnyJSON { var isPrimitive: Bool { switch self { case .null, .bool, .integer, .double, .string: - return true + true case .object, .array: - return false + false } } } diff --git a/Sources/Auth/AuthError.swift b/Sources/Auth/AuthError.swift index 102c2bce..1a3e41b2 100644 --- a/Sources/Auth/AuthError.swift +++ b/Sources/Auth/AuthError.swift @@ -45,15 +45,15 @@ public enum AuthError: LocalizedError, Sendable, Equatable { public var errorDescription: String? { switch self { - case let .api(error): return error.errorDescription ?? error.msg ?? error.error - case .missingExpClaim: return "Missing expiration claim on access token." - case .malformedJWT: return "A malformed JWT received." - case .sessionNotFound: return "Unable to get a valid session." - case .pkce(.codeVerifierNotFound): return "A code verifier wasn't found in PKCE flow." - case .pkce(.invalidPKCEFlowURL): return "Not a valid PKCE flow url." - case .invalidImplicitGrantFlowURL: return "Not a valid implicit grant flow url." - case .missingURL: return "Missing URL." - case .invalidRedirectScheme: return "Invalid redirect scheme." + case let .api(error): error.errorDescription ?? error.msg ?? error.error + case .missingExpClaim: "Missing expiration claim on access token." + case .malformedJWT: "A malformed JWT received." + case .sessionNotFound: "Unable to get a valid session." + case .pkce(.codeVerifierNotFound): "A code verifier wasn't found in PKCE flow." + case .pkce(.invalidPKCEFlowURL): "Not a valid PKCE flow url." + case .invalidImplicitGrantFlowURL: "Not a valid implicit grant flow url." + case .missingURL: "Missing URL." + case .invalidRedirectScheme: "Invalid redirect scheme." } } } diff --git a/Sources/Auth/Types.swift b/Sources/Auth/Types.swift index 6591f3e1..6e6e3893 100644 --- a/Sources/Auth/Types.swift +++ b/Sources/Auth/Types.swift @@ -413,8 +413,8 @@ public enum AuthResponse: Codable, Hashable, Sendable { public var user: User { switch self { - case let .session(session): return session.user - case let .user(user): return user + case let .session(session): session.user + case let .user(user): user } } diff --git a/Sources/Functions/Types.swift b/Sources/Functions/Types.swift index 574e8968..f3c4ad44 100644 --- a/Sources/Functions/Types.swift +++ b/Sources/Functions/Types.swift @@ -11,9 +11,9 @@ public enum FunctionsError: Error, LocalizedError { public var errorDescription: String? { switch self { case .relayError: - return "Relay Error invoking the Edge Function" + "Relay Error invoking the Edge Function" case let .httpError(code, _): - return "Edge Function returned a non-2xx status code: \(code)" + "Edge Function returned a non-2xx status code: \(code)" } } } diff --git a/Sources/PostgREST/Deprecated.swift b/Sources/PostgREST/Deprecated.swift index 085d7dac..d2ffb46b 100644 --- a/Sources/PostgREST/Deprecated.swift +++ b/Sources/PostgREST/Deprecated.swift @@ -78,13 +78,3 @@ extension PostgrestClient { ) } } - -extension PostgrestFilterBuilder { - @available(*, deprecated, renamed: "textSearch(_:value:)") - public func textSearch( - _ column: String, - range: any URLQueryRepresentable - ) -> PostgrestFilterBuilder { - textSearch(column, value: range) - } -} diff --git a/Sources/PostgREST/PostgrestClient.swift b/Sources/PostgREST/PostgrestClient.swift index c0f5aad5..e1dd6372 100644 --- a/Sources/PostgREST/PostgrestClient.swift +++ b/Sources/PostgREST/PostgrestClient.swift @@ -26,13 +26,13 @@ public final class PostgrestClient: Sendable { let logger: (any SupabaseLogger)? - /// Initializes a new configuration for the PostgREST client. + /// Creates a PostgREST client. /// - Parameters: - /// - url: The URL of the PostgREST server. - /// - schema: The schema to use. - /// - headers: The headers to include in requests. + /// - url: URL of the PostgREST endpoint. + /// - schema: Postgres schema to switch to. + /// - headers: Custom headers. /// - logger: The logger to use. - /// - fetch: The fetch handler to use for requests. + /// - fetch: Custom fetch. /// - encoder: The JSONEncoder to use for encoding. /// - decoder: The JSONDecoder to use for decoding. public init( @@ -68,11 +68,11 @@ public final class PostgrestClient: Sendable { /// Creates a PostgREST client with the specified parameters. /// - Parameters: - /// - url: The URL of the PostgREST server. - /// - schema: The schema to use. - /// - headers: The headers to include in requests. + /// - url: URL of the PostgREST endpoint. + /// - schema: Postgres schema to switch to. + /// - headers: Custom headers. /// - logger: The logger to use. - /// - session: The URLSession to use for requests. + /// - fetch: Custom fetch. /// - encoder: The JSONEncoder to use for encoding. /// - decoder: The JSONDecoder to use for decoding. public convenience init( @@ -110,9 +110,8 @@ public final class PostgrestClient: Sendable { return self } - /// Performs a query on a table or a view. + /// Perform a query on a table or a view. /// - Parameter table: The table or view name to query. - /// - Returns: A PostgrestQueryBuilder instance. public func from(_ table: String) -> PostgrestQueryBuilder { PostgrestQueryBuilder( configuration: configuration, @@ -120,14 +119,11 @@ public final class PostgrestClient: Sendable { ) } - /// Performs a function call. + /// Perform a function call. /// - Parameters: /// - fn: The function name to call. /// - params: The parameters to pass to the function call. - /// - count: Count algorithm to use to count rows returned by the function. - /// Only applicable for set-returning functions. - /// - Returns: A PostgrestFilterBuilder instance. - /// - Throws: An error if the function call fails. + /// - count: Count algorithm to use to count rows returned by the function. Only applicable for [set-returning functions](https://www.postgresql.org/docs/current/functions-srf.html). public func rpc( _ fn: String, params: some Encodable & Sendable, @@ -139,13 +135,10 @@ public final class PostgrestClient: Sendable { ).rpc(params: params, count: count) } - /// Performs a function call. + /// Perform a function call. /// - Parameters: /// - fn: The function name to call. - /// - count: Count algorithm to use to count rows returned by the function. - /// Only applicable for set-returning functions. - /// - Returns: A PostgrestFilterBuilder instance. - /// - Throws: An error if the function call fails. + /// - count: Count algorithm to use to count rows returned by the function. Only applicable for [set-returning functions](https://www.postgresql.org/docs/current/functions-srf.html). public func rpc( _ fn: String, count: CountOption? = nil diff --git a/Sources/PostgREST/PostgrestFilterBuilder.swift b/Sources/PostgREST/PostgrestFilterBuilder.swift index 576db753..f851a255 100644 --- a/Sources/PostgREST/PostgrestFilterBuilder.swift +++ b/Sources/PostgREST/PostgrestFilterBuilder.swift @@ -9,9 +9,11 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder { // MARK: - Filters - public func not(_ column: String, operator op: Operator, value: any URLQueryRepresentable) - -> PostgrestFilterBuilder - { + public func not( + _ column: String, + operator op: Operator, + value: any URLQueryRepresentable + ) -> PostgrestFilterBuilder { let queryValue = value.queryValue mutableState.withValue { @@ -36,7 +38,17 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder { return self } - public func eq(_ column: String, value: any URLQueryRepresentable) -> PostgrestFilterBuilder { + /// Match only rows where `column` is equal to `value`. + /// + /// To check if the value of `column` is NULL, you should use `is()` instead. + /// + /// - Parameters: + /// - column: The column to filter on + /// - value: The value to filter with + public func eq( + _ column: String, + value: any URLQueryRepresentable + ) -> PostgrestFilterBuilder { let queryValue = value.queryValue mutableState.withValue { $0.request.query.append(URLQueryItem(name: column, value: "eq.\(queryValue)")) @@ -44,7 +56,15 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder { return self } - public func neq(_ column: String, value: any URLQueryRepresentable) -> PostgrestFilterBuilder { + /// Match only rows where `column` is not equal to `value`. + /// + /// - Parameters: + /// - column: The column to filter on + /// - value: The value to filter with + public func neq( + _ column: String, + value: any URLQueryRepresentable + ) -> PostgrestFilterBuilder { let queryValue = value.queryValue mutableState.withValue { $0.request.query.append(URLQueryItem(name: column, value: "neq.\(queryValue)")) @@ -52,7 +72,15 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder { return self } - public func gt(_ column: String, value: any URLQueryRepresentable) -> PostgrestFilterBuilder { + /// Match only rows where `column` is greater than `value`. + /// + /// - Parameters: + /// - column: The column to filter on + /// - value: The value to filter with + public func gt( + _ column: String, + value: any URLQueryRepresentable + ) -> PostgrestFilterBuilder { let queryValue = value.queryValue mutableState.withValue { $0.request.query.append(URLQueryItem(name: column, value: "gt.\(queryValue)")) @@ -60,7 +88,15 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder { return self } - public func gte(_ column: String, value: any URLQueryRepresentable) -> PostgrestFilterBuilder { + /// Match only rows where `column` is greater than or equal to `value`. + /// + /// - Parameters: + /// - column: The column to filter on + /// - value: The value to filter with + public func gte( + _ column: String, + value: any URLQueryRepresentable + ) -> PostgrestFilterBuilder { let queryValue = value.queryValue mutableState.withValue { $0.request.query.append(URLQueryItem(name: column, value: "gte.\(queryValue)")) @@ -68,7 +104,15 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder { return self } - public func lt(_ column: String, value: any URLQueryRepresentable) -> PostgrestFilterBuilder { + /// Match only rows where `column` is less than `value`. + /// + /// - Parameters: + /// - column: The column to filter on + /// - value: The value to filter with + public func lt( + _ column: String, + value: any URLQueryRepresentable + ) -> PostgrestFilterBuilder { let queryValue = value.queryValue mutableState.withValue { $0.request.query.append(URLQueryItem(name: column, value: "lt.\(queryValue)")) @@ -76,7 +120,15 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder { return self } - public func lte(_ column: String, value: any URLQueryRepresentable) -> PostgrestFilterBuilder { + /// Match only rows where `column` is less than or equal to `value`. + /// + /// - Parameters: + /// - column: The column to filter on + /// - value: The value to filter with + public func lte( + _ column: String, + value: any URLQueryRepresentable + ) -> PostgrestFilterBuilder { let queryValue = value.queryValue mutableState.withValue { $0.request.query.append(URLQueryItem(name: column, value: "lte.\(queryValue)")) @@ -84,23 +136,66 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder { return self } - public func like(_ column: String, value: any URLQueryRepresentable) -> PostgrestFilterBuilder { - let queryValue = value.queryValue + /// Match only rows where `column` matches `pattern` case-sensitively. + /// + /// - Parameters: + /// - column: The column to filter on + /// - pattern: The pattern to match with + public func like( + _ column: String, + pattern: any URLQueryRepresentable + ) -> PostgrestFilterBuilder { + let queryValue = pattern.queryValue mutableState.withValue { $0.request.query.append(URLQueryItem(name: column, value: "like.\(queryValue)")) } return self } - public func ilike(_ column: String, value: any URLQueryRepresentable) -> PostgrestFilterBuilder { - let queryValue = value.queryValue + @available(*, deprecated, renamed: "like(_:pattern:)") + public func like( + _ column: String, + value: any URLQueryRepresentable + ) -> PostgrestFilterBuilder { + like(column, pattern: value) + } + + /// Match only rows where `column` matches `pattern` case-insensitively. + /// + /// - Parameters: + /// - column: The column to filter on + /// - pattern: The pattern to match with + public func ilike( + _ column: String, + pattern: any URLQueryRepresentable + ) -> PostgrestFilterBuilder { + let queryValue = pattern.queryValue mutableState.withValue { $0.request.query.append(URLQueryItem(name: column, value: "ilike.\(queryValue)")) } return self } - public func `is`(_ column: String, value: any URLQueryRepresentable) -> PostgrestFilterBuilder { + @available(*, deprecated, renamed: "ilike(_:pattern:)") + public func ilike( + _ column: String, + value: any URLQueryRepresentable + ) -> PostgrestFilterBuilder { + ilike(column, pattern: value) + } + + /// Match only rows where `column` IS `value`. + /// + /// For non-boolean columns, this is only relevant for checking if the value of `column` is NULL by setting `value` to `null`. + /// For boolean columns, you can also set `value` to `true` or `false` and it will behave the same way as `.eq()`. + /// + /// - Parameters: + /// - column: The column to filter on + /// - value: The value to filter with + public func `is`( + _ column: String, + value: any URLQueryRepresentable + ) -> PostgrestFilterBuilder { let queryValue = value.queryValue mutableState.withValue { $0.request.query.append(URLQueryItem(name: column, value: "is.\(queryValue)")) @@ -108,19 +203,42 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder { return self } - public func `in`(_ column: String, value: [any URLQueryRepresentable]) -> PostgrestFilterBuilder { - let queryValue = value.map(\.queryValue) + /// Match only rows where `column` is included in the `values` array. + /// + /// - Parameters: + /// - column: The column to filter on + /// - value: The values array to filter with + public func `in`( + _ column: String, + values: [any URLQueryRepresentable] + ) -> PostgrestFilterBuilder { + let queryValues = values.map(\.queryValue) mutableState.withValue { $0.request.query.append( URLQueryItem( name: column, - value: "in.(\(queryValue.joined(separator: ",")))" + value: "in.(\(queryValues.joined(separator: ",")))" ) ) } return self } + @available(*, deprecated, renamed: "in(_:values:)") + public func `in`( + _ column: String, + value: [any URLQueryRepresentable] + ) -> PostgrestFilterBuilder { + `in`(column, values: value) + } + + /// Match only rows where `column` contains every element appearing in `value`. + /// + /// Only relevant for jsonb, array, and range columns. + /// + /// - Parameters: + /// - column: The jsonb, array, or range column to filter on + /// - value: The jsonb, array, or range value to filter with public func contains( _ column: String, value: any URLQueryRepresentable @@ -132,6 +250,13 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder { return self } + /// Match only rows where every element in `column` is less than any element in `range`. + /// + /// Only relevant for range columns. + /// + /// - Parameters: + /// - column: The range column to filter on + /// - range: The range to filter with public func rangeLt( _ column: String, range: any URLQueryRepresentable @@ -143,6 +268,13 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder { return self } + /// Match only rows where every element in `column` is greater than any element in `range`. + /// + /// Only relevant for range columns. + /// + /// - Parameters: + /// - column: The range column to filter on + /// - range: The range to filter with public func rangeGt( _ column: String, range: any URLQueryRepresentable @@ -154,6 +286,13 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder { return self } + /// Match only rows where every element in `column` is either contained in `range` or greater than any element in `range`. + /// + /// Only relevant for range columns. + /// + /// - Parameters: + /// - column: The range column to filter on + /// - range: The range to filter with public func rangeGte( _ column: String, range: any URLQueryRepresentable @@ -165,6 +304,13 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder { return self } + /// Match only rows where every element in `column` is either contained in `range` or less than any element in `range`. + /// + /// Only relevant for range columns. + /// + /// - Parameters: + /// - column: The range column to filter on + /// - range: The range to filter with public func rangeLte( _ column: String, range: any URLQueryRepresentable @@ -176,6 +322,13 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder { return self } + /// Match only rows where `column` is mutually exclusive to `range` and there can be no element between the two ranges. + /// + /// Only relevant for range columns. + /// + /// - Parameters: + /// - column: The range column to filter on + /// - range: The range to filter with public func rangeAdjacent( _ column: String, range: any URLQueryRepresentable @@ -187,6 +340,13 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder { return self } + /// Match only rows where `column` and `value` have an element in common. + /// + /// Only relevant for array and range columns. + /// + /// - Parameters: + /// - column: The array or range column to filter on + /// - value: The array or range value to filter with public func overlaps( _ column: String, value: any URLQueryRepresentable @@ -198,17 +358,15 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder { return self } - public func textSearch( - _ column: String, - value: any URLQueryRepresentable - ) -> PostgrestFilterBuilder { - let queryValue = value.queryValue - mutableState.withValue { - $0.request.query.append(URLQueryItem(name: column, value: "adj.\(queryValue)")) - } - return self - } - + /// Match only rows where `column` matches the query string in `query`. + /// + /// Only relevant for text and tsvector columns. + /// + /// - Parameters: + /// - column: The text or tsvector column to filter on + /// - query: The query text to match with + /// - config: The text search configuration to use + /// - type: Change how the `query` text is interpreted public func textSearch( _ column: String, query: any URLQueryRepresentable, @@ -231,77 +389,64 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder { query: any URLQueryRepresentable, config: String? = nil ) -> PostgrestFilterBuilder { - let queryValue = query.queryValue - mutableState.withValue { - $0.request.query.append(URLQueryItem( - name: column, - value: "fts\(config ?? "").\(queryValue)" - )) - } - return self + textSearch(column, query: query, config: config, type: nil) } + @available(*, deprecated, message: "Use textSearch(_:query:config:type) with .plain type.") public func plfts( _ column: String, query: any URLQueryRepresentable, config: String? = nil ) -> PostgrestFilterBuilder { - let queryValue = query.queryValue - mutableState.withValue { - $0.request.query.append(URLQueryItem( - name: column, - value: "plfts\(config ?? "").\(queryValue)" - )) - } - return self + textSearch(column, query: query, config: config, type: .plain) } + @available(*, deprecated, message: "Use textSearch(_:query:config:type) with .phrase type.") public func phfts( _ column: String, query: any URLQueryRepresentable, config: String? = nil ) -> PostgrestFilterBuilder { - let queryValue = query.queryValue - mutableState.withValue { - $0.request.query.append(URLQueryItem( - name: column, - value: "phfts\(config ?? "").\(queryValue)" - )) - } - return self + textSearch(column, query: query, config: config, type: .phrase) } + @available(*, deprecated, message: "Use textSearch(_:query:config:type) with .websearch type.") public func wfts( _ column: String, query: any URLQueryRepresentable, config: String? = nil ) -> PostgrestFilterBuilder { - let queryValue = query.queryValue - mutableState.withValue { - $0.request.query.append(URLQueryItem( - name: column, - value: "wfts\(config ?? "").\(queryValue)" - )) - } - return self + textSearch(column, query: query, config: config, type: .websearch) } + /// Match only rows which satisfy the filter. This is an escape hatch - you should use the specific filter methods wherever possible. + /// + /// Unlike most filters, `opearator` and `value` are used as-is and need to follow [PostgREST syntax](https://postgrest.org/en/stable/api.html#operators). You also need to make sure they are properly sanitized. + /// + /// - Parameters: + /// - column: The column to filter on + /// - operator: The operator to filter with, following PostgREST syntax + /// - value: The value to filter with, following PostgREST syntax public func filter( _ column: String, - operator: Operator, - value: any URLQueryRepresentable + operator: String, + value: String ) -> PostgrestFilterBuilder { - let queryValue = value.queryValue mutableState.withValue { $0.request.query.append(URLQueryItem( name: column, - value: "\(`operator`.rawValue).\(queryValue)" + value: "\(`operator`).\(value)" )) } return self } - public func match(_ query: [String: any URLQueryRepresentable]) -> PostgrestFilterBuilder { + /// Match only rows where each column in `query` keys is equal to its associated value. Shorthand for multiple `.eq()`s. + /// + /// - Parameter query: The object to filter with, with column names as keys mapped to their filter values + public func match( + _ query: [String: any URLQueryRepresentable] + ) -> PostgrestFilterBuilder { let query = query.mapValues(\.queryValue) mutableState.withValue { mutableState in for (key, value) in query { @@ -316,67 +461,105 @@ public class PostgrestFilterBuilder: PostgrestTransformBuilder { // MARK: - Filter Semantic Improvements - public func equals(_ column: String, value: String) -> PostgrestFilterBuilder { + public func equals( + _ column: String, + value: String + ) -> PostgrestFilterBuilder { eq(column, value: value) } - public func notEquals(_ column: String, value: String) -> PostgrestFilterBuilder { + public func notEquals( + _ column: String, + value: String + ) -> PostgrestFilterBuilder { neq(column, value: value) } - public func greaterThan(_ column: String, value: String) -> PostgrestFilterBuilder { + public func greaterThan( + _ column: String, + value: String + ) -> PostgrestFilterBuilder { gt(column, value: value) } - public func greaterThanOrEquals(_ column: String, value: String) -> PostgrestFilterBuilder { + public func greaterThanOrEquals( + _ column: String, + value: String + ) -> PostgrestFilterBuilder { gte(column, value: value) } - public func lowerThan(_ column: String, value: String) -> PostgrestFilterBuilder { + public func lowerThan( + _ column: String, + value: String + ) -> PostgrestFilterBuilder { lt(column, value: value) } - public func lowerThanOrEquals(_ column: String, value: String) -> PostgrestFilterBuilder { + public func lowerThanOrEquals( + _ column: String, + value: String + ) -> PostgrestFilterBuilder { lte(column, value: value) } - public func rangeLowerThan(_ column: String, range: String) -> PostgrestFilterBuilder { + public func rangeLowerThan( + _ column: String, + range: String + ) -> PostgrestFilterBuilder { rangeLt(column, range: range) } - public func rangeGreaterThan(_ column: String, value: String) -> PostgrestFilterBuilder { + public func rangeGreaterThan( + _ column: String, + value: String + ) -> PostgrestFilterBuilder { rangeGt(column, range: value) } - public func rangeGreaterThanOrEquals(_ column: String, value: String) -> PostgrestFilterBuilder { + public func rangeGreaterThanOrEquals( + _ column: String, + value: String + ) -> PostgrestFilterBuilder { rangeGte(column, range: value) } - public func rangeLowerThanOrEquals(_ column: String, value: String) -> PostgrestFilterBuilder { + public func rangeLowerThanOrEquals( + _ column: String, + value: String + ) -> PostgrestFilterBuilder { rangeLte(column, range: value) } - public func fullTextSearch(_ column: String, query: String, config: String? = nil) - -> PostgrestFilterBuilder - { + public func fullTextSearch( + _ column: String, + query: String, + config: String? = nil + ) -> PostgrestFilterBuilder { fts(column, query: query, config: config) } - public func plainToFullTextSearch(_ column: String, query: String, config: String? = nil) - -> PostgrestFilterBuilder - { + public func plainToFullTextSearch( + _ column: String, + query: String, + config: String? = nil + ) -> PostgrestFilterBuilder { plfts(column, query: query, config: config) } - public func phraseToFullTextSearch(_ column: String, query: String, config: String? = nil) - -> PostgrestFilterBuilder - { + public func phraseToFullTextSearch( + _ column: String, + query: String, + config: String? = nil + ) -> PostgrestFilterBuilder { phfts(column, query: query, config: config) } - public func webFullTextSearch(_ column: String, query: String, config: String? = nil) - -> PostgrestFilterBuilder - { + public func webFullTextSearch( + _ column: String, + query: String, + config: String? = nil + ) -> PostgrestFilterBuilder { wfts(column, query: query, config: config) } } diff --git a/Sources/PostgREST/PostgrestQueryBuilder.swift b/Sources/PostgREST/PostgrestQueryBuilder.swift index 592174e5..22be446f 100644 --- a/Sources/PostgREST/PostgrestQueryBuilder.swift +++ b/Sources/PostgREST/PostgrestQueryBuilder.swift @@ -2,12 +2,11 @@ import _Helpers import Foundation public final class PostgrestQueryBuilder: PostgrestBuilder { - /// Performs a vertical filtering with SELECT. + /// Perform a SELECT query on the table or view. /// - Parameters: - /// - columns: The columns to retrieve, separated by commas. - /// - head: When set to true, select will void data. - /// - count: Count algorithm to use to count rows in a table. - /// - Returns: A `PostgrestFilterBuilder` instance for further filtering or operations. + /// - columns: The columns to retrieve, separated by commas. Columns can be renamed when returned with `customName:columnName` + /// - head: When set to `true`, `data` will not be returned. Useful if you only need the count. + /// - count: Count algorithm to use to count rows in the table or view. public func select( _ columns: String = "*", head: Bool = false, @@ -41,13 +40,13 @@ public final class PostgrestQueryBuilder: PostgrestBuilder { return PostgrestFilterBuilder(self) } - /// Performs an INSERT into the table. + /// Performs an INSERT into the table or view. + /// + /// By default, inserted rows are not returned. To return it, chain the call with `.select()`. + /// /// - Parameters: - /// - values: The values to insert. - /// - returning: The returning options for the query. - /// - count: Count algorithm to use to count rows in a table. - /// - Returns: A `PostgrestFilterBuilder` instance for further filtering or operations. - /// - Throws: An error if the insert fails. + /// - values: The values to insert. Pass an object to insert a single row or an array to insert multiple rows. + /// - count: Count algorithm to use to count inserted rows. public func insert( _ values: some Encodable & Sendable, returning: PostgrestReturningOptions? = nil, @@ -84,15 +83,17 @@ public final class PostgrestQueryBuilder: PostgrestBuilder { return PostgrestFilterBuilder(self) } - /// Performs an UPSERT into the table. + /// Perform an UPDATE on the table or view. + /// + /// Depending on the column(s) passed to `onConflict`, `.upsert()` allows you to perform the equivalent of `.insert()` if a row with the corresponding `onConflict` columns doesn't exist, or if it does exist, perform an alternative action depending on `ignoreDuplicates`. + /// + /// By default, upserted rows are not returned. To return it, chain the call with `.select()`. + /// /// - Parameters: - /// - values: The values to insert. - /// - onConflict: The column(s) with a unique constraint to perform the UPSERT. - /// - returning: The returning options for the query. - /// - count: Count algorithm to use to count rows in a table. - /// - ignoreDuplicates: Specifies if duplicate rows should be ignored and not inserted. - /// - Returns: A `PostgrestFilterBuilder` instance for further filtering or operations. - /// - Throws: An error if the upsert fails. + /// - values: The values to upsert with. Pass an object to upsert a single row or an array to upsert multiple rows. + /// - onConflict: Comma-separated UNIQUE column(s) to specify how duplicate rows are determined. Two rows are duplicates if all the `onConflict` columns are equal. + /// - count: Count algorithm to use to count upserted rows. + /// - ignoreDuplicates: If `true`, duplicate rows are ignored. If `false`, duplicate rows are merged with existing rows. public func upsert( _ values: some Encodable & Sendable, onConflict: String? = nil, @@ -134,13 +135,13 @@ public final class PostgrestQueryBuilder: PostgrestBuilder { return PostgrestFilterBuilder(self) } - /// Performs an UPDATE on the table. + /// Perform an UPDATE on the table or view. + /// + /// By default, updated rows are not returned. To return it, chain the call with `.select()` after filters. + /// /// - Parameters: - /// - values: The values to update. - /// - returning: The returning options for the query. + /// - values: The values to update with. /// - count: Count algorithm to use to count rows in a table. - /// - Returns: A `PostgrestFilterBuilder` instance for further filtering or operations. - /// - Throws: An error if the update fails. public func update( _ values: some Encodable & Sendable, returning: PostgrestReturningOptions = .representation, @@ -163,11 +164,12 @@ public final class PostgrestQueryBuilder: PostgrestBuilder { return PostgrestFilterBuilder(self) } - /// Performs a DELETE on the table. + /// Perform a DELETE on the table or view. + /// + /// By default, deleted rows are not returned. To return it, chain the call with `.select()` after filters. + /// /// - Parameters: - /// - returning: The returning options for the query. - /// - count: Count algorithm to use to count rows in a table. - /// - Returns: A `PostgrestFilterBuilder` instance for further filtering or operations. + /// - count: Count algorithm to use to count deleted rows. public func delete( returning: PostgrestReturningOptions = .representation, count: CountOption? = nil diff --git a/Sources/PostgREST/PostgrestTransformBuilder.swift b/Sources/PostgREST/PostgrestTransformBuilder.swift index 208c9c0c..b10ce385 100644 --- a/Sources/PostgREST/PostgrestTransformBuilder.swift +++ b/Sources/PostgREST/PostgrestTransformBuilder.swift @@ -2,7 +2,10 @@ import _Helpers import Foundation public class PostgrestTransformBuilder: PostgrestBuilder { - /// Performs a vertical filtering with SELECT. + /// Perform a SELECT on the query result. + /// + /// By default, `.insert()`, `.update()`, `.upsert()`, and `.delete()` do not return modified rows. By calling this method, modified rows are returned in `value`. + /// /// - Parameters: /// - columns: The columns to retrieve, separated by commas. public func select(_ columns: String = "*") -> PostgrestTransformBuilder { @@ -30,12 +33,16 @@ public class PostgrestTransformBuilder: PostgrestBuilder { return self } - /// Orders the result with the specified `column`. + /// Order the query result by `column`. + /// + /// You can call this method multiple times to order by multiple columns. + /// You can order referenced tables, but it only affects the ordering of theparent table if you use `!inner` in the query. + /// /// - Parameters: - /// - column: The column to order on. + /// - column: The column to order by. /// - ascending: If `true`, the result will be in ascending order. - /// - nullsFirst: If `true`, `null`s appear first. - /// - referencedTable: The foreign table to use (if `column` is a foreign column). + /// - nullsFirst: If `true`, `null`s appear first. If `false`, `null`s appear last. + /// - referencedTable: Set this to order a referenced table by its columns. public func order( _ column: String, ascending: Bool = true, @@ -63,10 +70,10 @@ public class PostgrestTransformBuilder: PostgrestBuilder { return self } - /// Limits the result with the specified `count`. + /// Limits the query result by `count`. /// - Parameters: - /// - count: The maximum no. of rows to limit to. - /// - referencedTable: The foreign table to use (for foreign columns). + /// - count: The maximum number of rows to return. + /// - referencedTable: Set this to limit rows of referenced tables instead of the parent table. public func limit(_ count: Int, referencedTable: String? = nil) -> PostgrestTransformBuilder { mutableState.withValue { let key = referencedTable.map { "\($0).limit" } ?? "limit" @@ -79,14 +86,19 @@ public class PostgrestTransformBuilder: PostgrestBuilder { return self } - /// Limits the result to rows within the specified range, inclusive. + /// Limit the query result by starting at an offset (`from`) and ending at the offset (`from + to`). + /// + /// Only records within this range are returned. + /// This respects the query order and if there is no order clause the range could behave unexpectedly. + /// The `from` and `to` values are 0-based and inclusive: `range(from: 1, to: 3)` will include the second, third and fourth rows of the query. + /// /// - Parameters: - /// - lowerBounds: The starting index from which to limit the result, inclusive. - /// - upperBounds: The last index to which to limit the result, inclusive. - /// - referencedTable: The foreign table to use (for foreign columns). + /// - from: The starting index from which to limit the result. + /// - to: The last index to which to limit the result. + /// - referencedTable: Set this to limit rows of referenced tables instead of the parent table. public func range( - from lowerBounds: Int, - to upperBounds: Int, + from: Int, + to: Int, referencedTable: String? = nil ) -> PostgrestTransformBuilder { let keyOffset = referencedTable.map { "\($0).offset" } ?? "offset" @@ -94,21 +106,21 @@ public class PostgrestTransformBuilder: PostgrestBuilder { mutableState.withValue { if let index = $0.request.query.firstIndex(where: { $0.name == keyOffset }) { - $0.request.query[index] = URLQueryItem(name: keyOffset, value: "\(lowerBounds)") + $0.request.query[index] = URLQueryItem(name: keyOffset, value: "\(from)") } else { - $0.request.query.append(URLQueryItem(name: keyOffset, value: "\(lowerBounds)")) + $0.request.query.append(URLQueryItem(name: keyOffset, value: "\(from)")) } // Range is inclusive, so add 1 if let index = $0.request.query.firstIndex(where: { $0.name == keyLimit }) { $0.request.query[index] = URLQueryItem( name: keyLimit, - value: "\(upperBounds - lowerBounds + 1)" + value: "\(to - from + 1)" ) } else { $0.request.query.append(URLQueryItem( name: keyLimit, - value: "\(upperBounds - lowerBounds + 1)" + value: "\(to - from + 1)" )) } } @@ -116,8 +128,9 @@ public class PostgrestTransformBuilder: PostgrestBuilder { return self } - /// Retrieves only one row from the result. Result must be one row (e.g. using `limit`), otherwise - /// this will result in an error. + /// Return `value` as a single object instead of an array of objects. + /// + /// Query result must be one row (e.g. using `.limit(1)`), otherwise this returns an error. public func single() -> PostgrestTransformBuilder { mutableState.withValue { $0.request.headers["Accept"] = "application/vnd.pgrst.object+json" @@ -125,7 +138,7 @@ public class PostgrestTransformBuilder: PostgrestBuilder { return self } - /// Set the response type to CSV. + /// Return `value` as a string in CSV format. public func csv() -> PostgrestTransformBuilder { mutableState.withValue { $0.request.headers["Accept"] = "text/csv" diff --git a/Sources/PostgREST/Types.swift b/Sources/PostgREST/Types.swift index fac968ed..8621bed3 100644 --- a/Sources/PostgREST/Types.swift +++ b/Sources/PostgREST/Types.swift @@ -36,8 +36,11 @@ public struct PostgrestResponse: Sendable { /// Returns count as part of the response when specified. public enum CountOption: String, Sendable { + /// Exact but slow count algorithm. Performs a `COUNT(*)` under the hood. case exact + /// Approximated but fast count algorithm. Uses the Postgres statistics under the hood. case planned + /// Uses exact count for low numbers and planned count for high numbers. case estimated } diff --git a/Sources/PostgREST/URLQueryRepresentable.swift b/Sources/PostgREST/URLQueryRepresentable.swift index b974646c..1b25f2e2 100644 --- a/Sources/PostgREST/URLQueryRepresentable.swift +++ b/Sources/PostgREST/URLQueryRepresentable.swift @@ -32,6 +32,16 @@ extension Array: URLQueryRepresentable where Element: URLQueryRepresentable { } } +extension Optional: URLQueryRepresentable where Wrapped: URLQueryRepresentable { + public var queryValue: String { + if let value = self { + return value.queryValue + } + + return "NULL" + } +} + extension Dictionary: URLQueryRepresentable where Key: URLQueryRepresentable, diff --git a/Sources/Realtime/Defaults.swift b/Sources/Realtime/Defaults.swift index dcd7f3c3..e74f08bc 100644 --- a/Sources/Realtime/Defaults.swift +++ b/Sources/Realtime/Defaults.swift @@ -101,8 +101,8 @@ public enum ChannelEvent { static func isLifecyleEvent(_ event: String) -> Bool { switch event { - case join, leave, reply, error, close: return true - default: return false + case join, leave, reply, error, close: true + default: false } } } diff --git a/Sources/Realtime/RealtimeChannel.swift b/Sources/Realtime/RealtimeChannel.swift index 91a1b6d8..09596185 100644 --- a/Sources/Realtime/RealtimeChannel.swift +++ b/Sources/Realtime/RealtimeChannel.swift @@ -921,14 +921,12 @@ public class RealtimeChannel { let handledMessage = message - let bindings: [Binding] - - if ["insert", "update", "delete"].contains(typeLower) { - bindings = self.bindings.value["postgres_changes", default: []].filter { bind in + let bindings: [Binding] = if ["insert", "update", "delete"].contains(typeLower) { + self.bindings.value["postgres_changes", default: []].filter { bind in bind.filter["event"] == "*" || bind.filter["event"] == typeLower } } else { - bindings = self.bindings.value[typeLower, default: []].filter { bind in + self.bindings.value[typeLower, default: []].filter { bind in if ["broadcast", "presence", "postgres_changes"].contains(typeLower) { let bindEvent = bind.filter["event"]?.lowercased() diff --git a/Sources/Realtime/RealtimeClient.swift b/Sources/Realtime/RealtimeClient.swift index b23ca3fb..d7e06856 100644 --- a/Sources/Realtime/RealtimeClient.swift +++ b/Sources/Realtime/RealtimeClient.swift @@ -274,9 +274,9 @@ public class RealtimeClient: PhoenixTransportDelegate { /// - return: The socket protocol, wss or ws public var websocketProtocol: String { switch endPointUrl.scheme { - case "https": return "wss" - case "http": return "ws" - default: return endPointUrl.scheme ?? "" + case "https": "wss" + case "http": "ws" + default: endPointUrl.scheme ?? "" } } @@ -734,10 +734,10 @@ public class RealtimeClient: PhoenixTransportDelegate { let callback: (() throws -> Void) = { [weak self] in guard let self else { return } let body: [Any?] = [joinRef, ref, topic, event, payload] - let data = self.encode(body) + let data = encode(body) - self.logItems("push", "Sending \(String(data: data, encoding: String.Encoding.utf8) ?? "")") - self.connection?.send(data: data) + logItems("push", "Sending \(String(data: data, encoding: String.Encoding.utf8) ?? "")") + connection?.send(data: data) } /// If the socket is connected, then execute the callback immediately. @@ -1063,9 +1063,9 @@ extension RealtimeClient { var shouldReconnect: Bool { switch self { case .unknown, .abnormal: - return true + true case .clean, .temporary: - return false + false } } } diff --git a/Sources/Realtime/V2/CallbackManager.swift b/Sources/Realtime/V2/CallbackManager.swift index 22f67276..1c9aab55 100644 --- a/Sources/Realtime/V2/CallbackManager.swift +++ b/Sources/Realtime/V2/CallbackManager.swift @@ -174,9 +174,9 @@ enum RealtimeCallback { var id: Int { switch self { - case let .postgres(callback): return callback.id - case let .broadcast(callback): return callback.id - case let .presence(callback): return callback.id + case let .postgres(callback): callback.id + case let .broadcast(callback): callback.id + case let .presence(callback): callback.id } } } diff --git a/Sources/Realtime/V2/PostgresAction.swift b/Sources/Realtime/V2/PostgresAction.swift index b2a29bc9..ddd44fac 100644 --- a/Sources/Realtime/V2/PostgresAction.swift +++ b/Sources/Realtime/V2/PostgresAction.swift @@ -75,10 +75,10 @@ public enum AnyAction: PostgresAction, HasRawMessage { var wrappedAction: any PostgresAction & HasRawMessage { switch self { - case let .insert(action): return action - case let .update(action): return action - case let .delete(action): return action - case let .select(action): return action + case let .insert(action): action + case let .update(action): action + case let .delete(action): action + case let .select(action): action } } diff --git a/Sources/Realtime/V2/RealtimeChannelV2.swift b/Sources/Realtime/V2/RealtimeChannelV2.swift index 69201f59..0fcb9466 100644 --- a/Sources/Realtime/V2/RealtimeChannelV2.swift +++ b/Sources/Realtime/V2/RealtimeChannelV2.swift @@ -453,7 +453,7 @@ public actor RealtimeChannelV2 { /// Listen for broadcast messages sent by other clients within the same channel under a specific /// `event`. - public func broadcast(event: String) -> AsyncStream { + public func broadcastStream(event: String) -> AsyncStream { let (stream, continuation) = AsyncStream.makeStream() let id = callbackManager.addBroadcastCallback(event: event) { @@ -470,6 +470,13 @@ public actor RealtimeChannelV2 { return stream } + /// Listen for broadcast messages sent by other clients within the same channel under a specific + /// `event`. + @available(*, deprecated, renamed: "broadcastStream(event:)") + public func broadcast(event: String) -> AsyncStream { + broadcastStream(event: event) + } + @discardableResult private func push(_ message: RealtimeMessageV2) async -> PushStatus { let push = PushV2(channel: self, message: message) diff --git a/Sources/Realtime/V2/RealtimeMessageV2.swift b/Sources/Realtime/V2/RealtimeMessageV2.swift index 7b9e587c..13227bdd 100644 --- a/Sources/Realtime/V2/RealtimeMessageV2.swift +++ b/Sources/Realtime/V2/RealtimeMessageV2.swift @@ -25,26 +25,26 @@ public struct RealtimeMessageV2: Hashable, Codable, Sendable { public var eventType: EventType? { switch event { - case ChannelEvent.system where payload["status"]?.stringValue == "ok": return .system + case ChannelEvent.system where payload["status"]?.stringValue == "ok": .system case ChannelEvent.postgresChanges: - return .postgresChanges + .postgresChanges case ChannelEvent.broadcast: - return .broadcast + .broadcast case ChannelEvent.close: - return .close + .close case ChannelEvent.error: - return .error + .error case ChannelEvent.presenceDiff: - return .presenceDiff + .presenceDiff case ChannelEvent.presenceState: - return .presenceState + .presenceState case ChannelEvent.system where payload["message"]?.stringValue?.contains("access token has expired") == true: - return .tokenExpired + .tokenExpired case ChannelEvent.reply: - return .reply + .reply default: - return nil + nil } } diff --git a/Sources/_Helpers/AnyJSON/AnyJSON.swift b/Sources/_Helpers/AnyJSON/AnyJSON.swift index 89603243..70ea165c 100644 --- a/Sources/_Helpers/AnyJSON/AnyJSON.swift +++ b/Sources/_Helpers/AnyJSON/AnyJSON.swift @@ -26,13 +26,13 @@ public enum AnyJSON: Sendable, Codable, Hashable { /// `AnyJSON` instances. public var value: Any { switch self { - case .null: return NSNull() - case let .string(string): return string - case let .integer(val): return val - case let .double(val): return val - case let .object(dictionary): return dictionary.mapValues(\.value) - case let .array(array): return array.map(\.value) - case let .bool(bool): return bool + case .null: NSNull() + case let .string(string): string + case let .integer(val): val + case let .double(val): val + case let .object(dictionary): dictionary.mapValues(\.value) + case let .array(array): array.map(\.value) + case let .bool(bool): bool } } diff --git a/Sources/_Helpers/SupabaseLogger.swift b/Sources/_Helpers/SupabaseLogger.swift index de458f98..07cf9cf4 100644 --- a/Sources/_Helpers/SupabaseLogger.swift +++ b/Sources/_Helpers/SupabaseLogger.swift @@ -8,10 +8,10 @@ public enum SupabaseLogLevel: Int, Codable, CustomStringConvertible, Sendable { public var description: String { switch self { - case .verbose: return "verbose" - case .debug: return "debug" - case .warning: return "warning" - case .error: return "error" + case .verbose: "verbose" + case .debug: "debug" + case .warning: "warning" + case .error: "error" } } } diff --git a/Tests/PostgRESTTests/BuildURLRequestTests.swift b/Tests/PostgRESTTests/BuildURLRequestTests.swift index 329191e0..4a3d1432 100644 --- a/Tests/PostgRESTTests/BuildURLRequestTests.swift +++ b/Tests/PostgRESTTests/BuildURLRequestTests.swift @@ -79,7 +79,7 @@ final class BuildURLRequestTests: XCTestCase { TestCase(name: "select all users where email ends with '@supabase.co'") { client in client.from("users") .select() - .like("email", value: "%@supabase.co") + .like("email", pattern: "%@supabase.co") }, TestCase(name: "insert new user") { client in try client.from("users") @@ -107,13 +107,13 @@ final class BuildURLRequestTests: XCTestCase { var query = client.from("todos").select() for op in PostgrestFilterBuilder.Operator.allCases { - query = query.filter("column", operator: op, value: "Some value") + query = query.filter("column", operator: op.rawValue, value: "Some value") } return query }, TestCase(name: "test in filter") { client in - client.from("todos").select().in("id", value: [1, 2, 3]) + client.from("todos").select().in("id", values: [1, 2, 3]) }, TestCase(name: "test contains filter with dictionary") { client in client.from("users").select("name") @@ -167,6 +167,11 @@ final class BuildURLRequestTests: XCTestCase { .insert(User(email: "johndoe@supabase.io")) .select("id,email") }, + TestCase(name: "query if nil value") { client in + client.from("users") + .select() + .is("email", value: String?.none) + }, ] for testCase in testCases { diff --git a/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.query-if-nil-value.txt b/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.query-if-nil-value.txt new file mode 100644 index 00000000..cc3a34f0 --- /dev/null +++ b/Tests/PostgRESTTests/__Snapshots__/BuildURLRequestTests/testBuildRequest.query-if-nil-value.txt @@ -0,0 +1,5 @@ +curl \ + --header "Accept: application/json" \ + --header "Content-Type: application/json" \ + --header "X-Client-Info: postgrest-swift/x.y.z" \ + "https://example.supabase.co/users?email=is.NULL&select=*" \ No newline at end of file diff --git a/Tests/RealtimeTests/_PushTests.swift b/Tests/RealtimeTests/_PushTests.swift index 730852fd..5e493787 100644 --- a/Tests/RealtimeTests/_PushTests.swift +++ b/Tests/RealtimeTests/_PushTests.swift @@ -58,7 +58,7 @@ final class _PushTests: XCTestCase { XCTAssertEqual(status, .ok) } -// FIXME: Flaky test, it fails some time due the task scheduling, even tho we're using withMainSerialExecutor. + // FIXME: Flaky test, it fails some time due the task scheduling, even tho we're using withMainSerialExecutor. // func testPushWithAck() async { // let channel = RealtimeChannelV2( // topic: "realtime:users",