Skip to content

Commit

Permalink
Merge pull request #35 from phiHero/master
Browse files Browse the repository at this point in the history
Throw better errors, allow search grouping of multiple fields with different data type, bug fixes 🐛 , ...
  • Loading branch information
jasonbosco authored Aug 26, 2024
2 parents a04a85b + ee62aac commit 126815b
Show file tree
Hide file tree
Showing 60 changed files with 968 additions and 586 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
mkdir $(pwd)/typesense-data
docker run -p 8108:8108 \
-d \
-v$(pwd)/typesense-data:/data typesense/typesense:26.0 \
-v$(pwd)/typesense-data:/data typesense/typesense:27.0.rc35 \
--data-dir /data \
--api-key=xyz \
--enable-cors
Expand Down
16 changes: 16 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"object": {
"pins": [
{
"package": "AnyCodable",
"repositoryURL": "https://github.com/Flight-School/AnyCodable",
"state": {
"branch": null,
"revision": "862808b2070cd908cb04f9aafe7de83d35f81b05",
"version": "0.6.7"
}
}
]
},
"version": 1
}
6 changes: 5 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ let package = Package(
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(
url: "https://github.com/Flight-School/AnyCodable",
from: "0.6.0"
),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "Typesense",
dependencies: []),
dependencies: ["AnyCodable"]),
.testTarget(
name: "TypesenseTests",
dependencies: ["Typesense"]),
Expand Down
12 changes: 2 additions & 10 deletions Sources/Typesense/Alias.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@ import Foundation
public struct Alias {
var apiCall: ApiCall
let RESOURCEPATH = "aliases"
var config: Configuration

public init(config: Configuration) {
apiCall = ApiCall(config: config)
self.config = config
init(apiCall: ApiCall) {
self.apiCall = apiCall
}

public func upsert(name: String, collection: CollectionAliasSchema) async throws -> (CollectionAlias?, URLResponse?) {
Expand All @@ -31,9 +29,6 @@ public struct Alias {
public func retrieve(name: String) async throws -> (CollectionAlias?, URLResponse?) {
let (data, response) = try await apiCall.get(endPoint: "\(RESOURCEPATH)/\(name)")
if let result = data {
if let notExists = try? decoder.decode(ApiResponse.self, from: result) {
throw ResponseError.aliasNotFound(desc: "Alias \(notExists.message)")
}
let alias = try decoder.decode(CollectionAlias.self, from: result)
return (alias, response)
}
Expand All @@ -52,9 +47,6 @@ public struct Alias {
public func delete(name: String) async throws -> (CollectionAlias?, URLResponse?) {
let (data, response) = try await apiCall.delete(endPoint: "\(RESOURCEPATH)/\(name)")
if let result = data {
if let notExists = try? decoder.decode(ApiResponse.self, from: result) {
throw ResponseError.aliasNotFound(desc: "Alias \(notExists.message)")
}
let alias = try decoder.decode(CollectionAlias.self, from: result)
return (alias, response)
}
Expand Down
18 changes: 11 additions & 7 deletions Sources/Typesense/Analytics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,24 @@ import Foundation

public struct Analytics {
static let resourcePath: String = "/analytics"

private var analyticsRules: AnalyticsRules
var apiCall: ApiCall

init(config: Configuration) {
self.apiCall = ApiCall(config: config)
init(apiCall: ApiCall) {
self.apiCall = apiCall
self.analyticsRules = AnalyticsRules(apiCall: apiCall)
}

func rule(id: String) -> AnalyticsRule {

public func events() -> AnalyticsEvents {
return AnalyticsEvents(apiCall: self.apiCall)
}

public func rule(id: String) -> AnalyticsRule {
return AnalyticsRule(name: id, apiCall: self.apiCall)
}
func rules() -> AnalyticsRules {

public func rules() -> AnalyticsRules {
return AnalyticsRules(apiCall: self.apiCall)
}
}
Expand Down
23 changes: 23 additions & 0 deletions Sources/Typesense/AnalyticsEvents.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

public struct AnalyticsEvents {
static var resourcePath: String = "\(Analytics.resourcePath)/events"
var apiCall: ApiCall

init(apiCall: ApiCall) {
self.apiCall = apiCall
}

public func create<T: Encodable>(params: AnalyticsEventCreateSchema<T>) async throws -> (AnalyticsEventCreateResponse?, URLResponse?) {
let json = try encoder.encode(params)
let (data, response) = try await self.apiCall.post(endPoint: AnalyticsEvents.resourcePath, body: json)
if let result = data {
let validData = try decoder.decode(AnalyticsEventCreateResponse.self, from: result)
return (validData, response)
}
return (nil, response)
}
}
10 changes: 2 additions & 8 deletions Sources/Typesense/AnalyticsRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,16 @@ public struct AnalyticsRule {
public func retrieve() async throws -> (AnalyticsRuleSchema?, URLResponse?) {
let (data, response) = try await self.apiCall.get(endPoint: endpointPath())
if let result = data {
if let notFound = try? decoder.decode(ApiResponse.self, from: result) {
throw ResponseError.analyticsRuleDoesNotExist(desc: notFound.message)
}
let fetchedRule = try decoder.decode(AnalyticsRuleSchema.self, from: result)
return (fetchedRule, response)
}
return (nil, response)
}

public func delete() async throws -> (AnalyticsRuleSchema?, URLResponse?) {
public func delete() async throws -> (AnalyticsRuleDeleteResponse?, URLResponse?) {
let (data, response) = try await self.apiCall.delete(endPoint: endpointPath())
if let result = data {
if let notFound = try? decoder.decode(ApiResponse.self, from: result) {
throw ResponseError.analyticsRuleDoesNotExist(desc: notFound.message)
}
let deletedRule = try decoder.decode(AnalyticsRuleSchema.self, from: result)
let deletedRule = try decoder.decode(AnalyticsRuleDeleteResponse.self, from: result)
return (deletedRule, response)
}
return (nil, response)
Expand Down
4 changes: 2 additions & 2 deletions Sources/Typesense/AnalyticsRules.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public struct AnalyticsRules {
self.apiCall = apiCall
}

func upsert(params: AnalyticsRuleSchema) async throws -> (AnalyticsRuleSchema?, URLResponse?) {
public func upsert(params: AnalyticsRuleSchema) async throws -> (AnalyticsRuleSchema?, URLResponse?) {
let ruleData = try encoder.encode(params)
let (data, response) = try await self.apiCall.put(endPoint: endpointPath(params.name), body: ruleData)
if let result = data {
Expand All @@ -23,7 +23,7 @@ public struct AnalyticsRules {
return (nil, response)
}

func retrieveAll() async throws -> (AnalyticsRulesRetrieveSchema?, URLResponse?) {
public func retrieveAll() async throws -> (AnalyticsRulesRetrieveSchema?, URLResponse?) {
let (data, response) = try await self.apiCall.get(endPoint: endpointPath())
if let result = data {
let rules = try decoder.decode(AnalyticsRulesRetrieveSchema.self, from: result)
Expand Down
37 changes: 27 additions & 10 deletions Sources/Typesense/ApiCall.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@ import Foundation
let APIKEYHEADERNAME = "X-Typesense-Api-Key"
let HEALTHY = true
let UNHEALTHY = false
private var currentNodeIndex = -1

struct ApiCall {
class ApiCall {
var nodes: [Node]
var apiKey: String
var nearestNode: Node? = nil
Expand All @@ -19,6 +18,7 @@ struct ApiCall {
var retryIntervalSeconds: Float = 0.1
var sendApiKeyAsQueryParam: Bool = false
var logger: Logger
var currentNodeIndex = -1

init(config: Configuration) {
self.apiKey = config.apiKey
Expand Down Expand Up @@ -62,9 +62,11 @@ struct ApiCall {
}

//Actual API Call
func performRequest(requestType: RequestType, endpoint: String, body: Data? = nil, queryParameters: [URLQueryItem]? = nil) async throws -> (Data?, URLResponse?) {
func performRequest(requestType: RequestType, endpoint: String, body: Data? = nil, queryParameters: [URLQueryItem]? = nil) async throws -> (Data, URLResponse) {
let requestNumber = Date().millisecondsSince1970
logger.log("Request #\(requestNumber): Performing \(requestType.rawValue) request: /\(endpoint)")
var lastError: any Error = HTTPError.clientError(code: 400, desc: "Typesense client error!")

for numTry in 1...self.numRetries + 1 {
//Get next healthy node
var selectedNode = self.getNextNode(requestNumber: requestNumber)
Expand All @@ -84,17 +86,23 @@ struct ApiCall {

logger.log("Request \(requestNumber): Request to \(request.url!) was made. Response Code was \(res.statusCode)")

if (res.statusCode < 500) {
//For any response under code 500, return the corresponding HTTP Response without retries
if (res.statusCode >= 200 && res.statusCode < 300) {
// if code is 2xx return the corresponding HTTP Response without retries
return (data, response)
}
else if (res.statusCode < 500) {
// Don't retry if code is not 5xx
throw HTTPClientError(data: data, statusCode: res.statusCode)
} else {
//For all other response codes (>=500) throw custom error
throw HTTPError.serverError(code: res.statusCode, desc: res.debugDescription)
}

}
} catch HTTPError.clientError(let code, let desc){
throw HTTPError.clientError(code: code, desc: desc)
} catch (let error) {

lastError = error
selectedNode = setNodeHealthCheck(node: selectedNode, isHealthy: UNHEALTHY)
logger.log("Request \(requestNumber): Request to \(selectedNode)/\(endpoint) failed with error: \(error.localizedDescription)")
logger.log("Request \(requestNumber): Sleeping for \(retryIntervalSeconds) seconds and retrying")
Expand All @@ -103,8 +111,7 @@ struct ApiCall {
}

}

return (nil,nil)
throw lastError
}

//Bundles a URL Request
Expand Down Expand Up @@ -169,7 +176,6 @@ struct ApiCall {

logger.log("Request #\(requestNumber): Falling back to individual nodes")
}

//Fallback to nodes as usual
logger.log("Request #\(requestNumber): Listing health of nodes")
let _ = self.nodes.map { node in
Expand Down Expand Up @@ -209,7 +215,7 @@ struct ApiCall {
}

//Initializes a node's health status and last access time
mutating func initializeMetadataForNodes() {
func initializeMetadataForNodes() {
if let existingNearestNode = self.nearestNode {
self.nearestNode = self.setNodeHealthCheck(node: existingNearestNode, isHealthy: HEALTHY)
}
Expand All @@ -219,4 +225,15 @@ struct ApiCall {
}
}

func HTTPClientError(data: Data, statusCode: Int) -> Error {
let decodedMessage = try? decoder.decode(ApiResponse.self, from: data)

var errorMessage = "Request failed with HTTP code \(statusCode)"
if let validMessage = decodedMessage{
errorMessage += " | Server said: \(validMessage.message)"
}

return HTTPError.clientError(code: statusCode, desc: errorMessage)
}

}
12 changes: 2 additions & 10 deletions Sources/Typesense/ApiKeys.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ public struct ApiKeys {
var apiCall: ApiCall
let RESOURCEPATH = "keys"

public init(config: Configuration) {
apiCall = ApiCall(config: config)
init(apiCall: ApiCall) {
self.apiCall = apiCall
}

public func create(_ keySchema: ApiKeySchema) async throws -> (ApiKey?, URLResponse?) {
Expand All @@ -32,9 +32,6 @@ public struct ApiKeys {

let (data, response) = try await apiCall.get(endPoint: "\(RESOURCEPATH)/\(id)")
if let result = data {
if let notFound = try? decoder.decode(ApiResponse.self, from: result) {
throw ResponseError.apiKeyNotFound(desc: notFound.message)
}
let keyResponse = try decoder.decode(ApiKey.self, from: result)
return (keyResponse, response)
}
Expand All @@ -56,11 +53,6 @@ public struct ApiKeys {
public func delete(id: Int) async throws -> (Data?, URLResponse?) {

let (data, response) = try await apiCall.delete(endPoint: "\(RESOURCEPATH)/\(id)")
if let result = data {
if let notFound = try? decoder.decode(ApiResponse.self, from: result) {
throw ResponseError.apiKeyNotFound(desc: notFound.message)
}
}
return (data, response)
}
}
14 changes: 7 additions & 7 deletions Sources/Typesense/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,31 @@ public struct Client {
public init(config: Configuration) {
self.configuration = config
self.apiCall = ApiCall(config: config)
self.collections = Collections(config: config)
self.collections = Collections(apiCall: apiCall)
}

public func collection(name: String) -> Collection {
return Collection(config: self.configuration, collectionName: name)
return Collection(apiCall: apiCall, collectionName: name)
}

public func keys() -> ApiKeys {
return ApiKeys(config: self.configuration)
return ApiKeys(apiCall: apiCall)
}

public func aliases() -> Alias {
return Alias(config: self.configuration)
return Alias(apiCall: apiCall)
}

public func operations() -> Operations {
return Operations(config: self.configuration)
return Operations(apiCall: apiCall)
}

public func multiSearch() -> MultiSearch {
return MultiSearch(config: self.configuration)
return MultiSearch(apiCall: apiCall)
}

public func analytics() -> Analytics {
return Analytics(config: self.configuration)
return Analytics(apiCall: apiCall)
}

public func presets() -> Presets {
Expand Down
Loading

0 comments on commit 126815b

Please sign in to comment.