├── Tests └── FileMakerNIOTests │ └── FileMakerNIOTests.swift ├── .gitignore ├── Sources └── FileMakerNIO │ ├── Models │ ├── EmptyRequest.swift │ ├── FileMakerError.swift │ ├── Requests │ │ └── CreateRecordRequest.swift │ ├── FileMakerNIOError.swift │ ├── FMIdentifiable.swift │ ├── Responses │ │ ├── CodableAction.swift │ │ ├── DeleteRecordResponse.swift │ │ ├── LoginResponse.swift │ │ ├── EditRecordResponse.swift │ │ ├── FileMakerResponse.swift │ │ ├── CreateRecordResponse.swift │ │ ├── DuplicateRecordResponse.swift │ │ ├── FindRecordsResponse.swift │ │ ├── GetRecordResponse.swift │ │ ├── GetRangeOfRecordsResponse.swift │ │ └── GetRecordData.swift │ └── PropertyWrappers │ │ ├── FMInt.swift │ │ ├── FMTime.swift │ │ ├── FMDate.swift │ │ └── FMTimestamp.swift │ ├── FileMakerConiguration.swift │ ├── Client │ ├── Client.swift │ └── HTTPClient+Requester.swift │ ├── FileMakerNIO.swift │ └── Operations │ ├── FileMakerAuthentication.swift │ ├── RecordOperations.swift │ └── Operations.swift ├── README.md ├── .github └── workflows │ └── ci.yml ├── Package.swift └── Package.resolved /Tests/FileMakerNIOTests/FileMakerNIOTests.swift: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData 7 | .swiftpm 8 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Models/EmptyRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct EmptyRequest: Codable {} 4 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Models/FileMakerError.swift: -------------------------------------------------------------------------------- 1 | public struct FileMakerError: Error { 2 | public let errorCode: String 3 | public let message: String 4 | } 5 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Models/Requests/CreateRecordRequest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct CreateRecordRequest: Encodable where T: Encodable { 4 | let fieldData: T 5 | } 6 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Models/FileMakerNIOError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AsyncHTTPClient 3 | 4 | public struct FileMakerNIOError: Error { 5 | public let message: String 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FileMakerNIO 2 | 3 | A Swift FileMaker Data API wrapper built on top of [SwiftNIO](https://github.com/apple/swift-nio) and [Async HTTP Client](https://github.com/swift-server/async-http-client). 4 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Models/FMIdentifiable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol FMIdentifiable: class, Codable { 4 | var recordId: Int? { get set } 5 | var modId: Int? { get set } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Models/Responses/CodableAction.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIOHTTP1 3 | 4 | protocol CodableAction: Codable { 5 | static var action: String { get } 6 | static var method: HTTPMethod { get } 7 | } 8 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Models/Responses/DeleteRecordResponse.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | public struct DeleteRecordResponse: CodableAction { 4 | static let action = "delete record" 5 | static let method = HTTPMethod.DELETE 6 | } 7 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Models/Responses/LoginResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct LoginResponse: Codable { 4 | let response: LoginResponseToken 5 | } 6 | 7 | struct LoginResponseToken: Codable { 8 | let token: String 9 | } 10 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Models/Responses/EditRecordResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIOHTTP1 3 | 4 | public struct EditRecordResponse: CodableAction { 5 | static let action = "edit record" 6 | static let method = HTTPMethod.PATCH 7 | public let modId: String 8 | } 9 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Models/Responses/FileMakerResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct FileMakerResponse: Codable { 4 | let response: T 5 | let messages: [ResponseMessage] 6 | } 7 | 8 | struct ResponseMessage: Codable { 9 | let code: String 10 | let message: String 11 | } 12 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Models/Responses/CreateRecordResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIOHTTP1 3 | 4 | public struct CreateRecordResponse: CodableAction { 5 | static let action = "create record" 6 | static let method = HTTPMethod.POST 7 | public let recordId: String 8 | public let modId: String 9 | } 10 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Models/Responses/DuplicateRecordResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIOHTTP1 3 | 4 | public struct DuplicateRecordResponse: CodableAction { 5 | static let action = "duplicate record" 6 | static let method = HTTPMethod.POST 7 | public let recordId: String 8 | public let modId: String 9 | } 10 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Models/Responses/FindRecordsResponse.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | public struct FindRecordsResponse: CodableAction { 4 | static var method: HTTPMethod { 5 | .POST 6 | } 7 | static var action: String { 8 | "find records" 9 | } 10 | let data: [GetRecordData] 11 | } 12 | 13 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Models/Responses/GetRecordResponse.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | import Foundation 3 | 4 | struct GetRecordResponse: CodableAction { 5 | static var method: HTTPMethod { 6 | .GET 7 | } 8 | static var action: String { 9 | "get record" 10 | } 11 | let data: [GetRecordData] 12 | } 13 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Models/Responses/GetRangeOfRecordsResponse.swift: -------------------------------------------------------------------------------- 1 | import NIOHTTP1 2 | 3 | public struct GetRangeOfRecordsResponse: CodableAction { 4 | static var method: HTTPMethod { 5 | .GET 6 | } 7 | static var action: String { 8 | "get range of records" 9 | } 10 | let data: [GetRecordData] 11 | } 12 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Models/Responses/GetRecordData.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct GetRecordData: Codable { 4 | let recordId: String 5 | let modId: String 6 | let fieldData: T 7 | } 8 | 9 | extension GetRecordData { 10 | func completeModel() -> T { 11 | let model = self.fieldData 12 | model.modId = Int(self.modId) 13 | model.recordId = Int(self.recordId) 14 | return model 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | jobs: 5 | xenial: 6 | container: 7 | image: vapor/swift:5.2-xenial 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@master 11 | - run: swift test --enable-test-discovery --enable-code-coverage --sanitize=thread 12 | bionic: 13 | container: 14 | image: vapor/swift:5.2-bionic 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@master 18 | - name: Run Bionic Tests 19 | run: swift test --enable-test-discovery --enable-code-coverage --sanitize=thread 20 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/FileMakerConiguration.swift: -------------------------------------------------------------------------------- 1 | public struct FileMakerConfiguration { 2 | public let scheme: String 3 | public let hostname: String 4 | public let port: String 5 | public let databaseName: String 6 | public let username: String 7 | public let password: String 8 | 9 | public init(scheme: String = "https", hostname: String, port: String = "443", databaseName: String, username: String, password: String) { 10 | self.scheme = scheme 11 | self.hostname = hostname 12 | self.port = port 13 | self.databaseName = databaseName 14 | self.username = username 15 | self.password = password 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Client/Client.swift: -------------------------------------------------------------------------------- 1 | import AsyncHTTPClient 2 | import NIO 3 | import NIOHTTP1 4 | import Logging 5 | 6 | public protocol Client { 7 | var eventLoopGroup: EventLoopGroup { get } 8 | func sendRequest(to url: String, method: HTTPMethod, data: T, sessionToken: String?, basicAuth: BasicAuthCredentials?, logger: Logger, eventLoop: EventLoop) -> EventLoopFuture 9 | func sendRequest(to url: String, method: HTTPMethod, sessionToken: String?, logger: Logger, eventLoop: EventLoop) -> EventLoopFuture 10 | } 11 | 12 | public struct BasicAuthCredentials { 13 | public let username: String 14 | public let password: String 15 | } 16 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "filemaker-nio", 8 | products: [ 9 | .library( 10 | name: "FileMakerNIO", 11 | targets: ["FileMakerNIO"]), 12 | ], 13 | dependencies: [ 14 | .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.0.0"), 15 | .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0") 16 | ], 17 | targets: [ 18 | .target( 19 | name: "FileMakerNIO", 20 | dependencies: [ 21 | .product(name: "AsyncHTTPClient", package: "async-http-client"), 22 | .product(name: "Logging", package: "swift-log") 23 | ]), 24 | .testTarget( 25 | name: "FileMakerNIOTests", 26 | dependencies: ["FileMakerNIO"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Models/PropertyWrappers/FMInt.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @propertyWrapper 4 | public final class FMInt: Codable { 5 | 6 | public var value: Int? 7 | 8 | public init(wrappedValue value: Int?) { 9 | self.value = value 10 | } 11 | 12 | public var wrappedValue: Int? { 13 | get { value } 14 | set { value = newValue } 15 | } 16 | 17 | public init(from decoder: Decoder) throws { 18 | let container = try decoder.singleValueContainer() 19 | if let intValue = try? container.decode(Int.self) { 20 | self.wrappedValue = intValue 21 | } else { 22 | let stringValue = try container.decode(String.self) 23 | if stringValue == "" { 24 | self.wrappedValue = nil 25 | } else { 26 | throw FileMakerNIOError(message: "Invalid type when decoding FMInt") 27 | } 28 | } 29 | } 30 | 31 | public func encode(to encoder: Encoder) throws { 32 | var container = encoder.singleValueContainer() 33 | try container.encode(self.wrappedValue) 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Models/PropertyWrappers/FMTime.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @propertyWrapper 4 | public final class FMTime: Codable { 5 | 6 | public var value: Date? 7 | 8 | public init(wrappedValue value: Date?) { 9 | self.value = value 10 | } 11 | 12 | public var wrappedValue: Date? { 13 | get { value } 14 | set { value = newValue } 15 | } 16 | 17 | public init(from decoder: Decoder) throws { 18 | let container = try decoder.singleValueContainer() 19 | let stringValue = try container.decode(String.self) 20 | if stringValue == "" { 21 | self.wrappedValue = nil 22 | } else { 23 | let dateFormat = DateFormatter() 24 | dateFormat.dateFormat = "HH:mm:SS" 25 | self.wrappedValue = dateFormat.date(from: stringValue) 26 | } 27 | } 28 | 29 | public func encode(to encoder: Encoder) throws { 30 | var container = encoder.singleValueContainer() 31 | if let date = self.wrappedValue { 32 | let dateFormat = DateFormatter() 33 | dateFormat.dateFormat = "HH:mm:SS" 34 | let dateAsString = dateFormat.string(from: date) 35 | try container.encode(dateAsString) 36 | } else { 37 | try container.encodeNil() 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Models/PropertyWrappers/FMDate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @propertyWrapper 4 | public final class FMDate: Codable { 5 | 6 | public var value: Date? 7 | 8 | public init(wrappedValue value: Date?) { 9 | self.value = value 10 | } 11 | 12 | public var wrappedValue: Date? { 13 | get { value } 14 | set { value = newValue } 15 | } 16 | 17 | public init(from decoder: Decoder) throws { 18 | let container = try decoder.singleValueContainer() 19 | let stringValue = try container.decode(String.self) 20 | if stringValue == "" { 21 | self.wrappedValue = nil 22 | } else { 23 | let dateFormat = DateFormatter() 24 | dateFormat.dateFormat = "dd/MM/YYYY" 25 | self.wrappedValue = dateFormat.date(from: stringValue) 26 | } 27 | } 28 | 29 | public func encode(to encoder: Encoder) throws { 30 | var container = encoder.singleValueContainer() 31 | if let date = self.wrappedValue { 32 | let dateFormat = DateFormatter() 33 | dateFormat.dateFormat = "dd/MM/YYYY" 34 | let dateAsString = dateFormat.string(from: date) 35 | try container.encode(dateAsString) 36 | } else { 37 | try container.encodeNil() 38 | } 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Models/PropertyWrappers/FMTimestamp.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @propertyWrapper 4 | public final class FMTimestamp: Codable { 5 | 6 | public var value: Date? 7 | 8 | public init(wrappedValue value: Date?) { 9 | self.value = value 10 | } 11 | 12 | public var wrappedValue: Date? { 13 | get { value } 14 | set { value = newValue } 15 | } 16 | 17 | public init(from decoder: Decoder) throws { 18 | let container = try decoder.singleValueContainer() 19 | let stringValue = try container.decode(String.self) 20 | if stringValue == "" { 21 | self.wrappedValue = nil 22 | } else { 23 | let dateFormat = DateFormatter() 24 | dateFormat.dateFormat = "dd/MM/yyyy HH:mm:SS" 25 | self.wrappedValue = dateFormat.date(from: stringValue) 26 | } 27 | } 28 | 29 | public func encode(to encoder: Encoder) throws { 30 | var container = encoder.singleValueContainer() 31 | if let date = self.wrappedValue { 32 | let dateFormat = DateFormatter() 33 | dateFormat.dateFormat = "dd/MM/yyyy HH:mm:SS" 34 | let dateAsString = dateFormat.string(from: date) 35 | try container.encode(dateAsString) 36 | } else { 37 | try container.encodeNil() 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "async-http-client", 6 | "repositoryURL": "https://github.com/swift-server/async-http-client.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "037b70291941fe43de668066eb6fb802c5e181d2", 10 | "version": "1.1.1" 11 | } 12 | }, 13 | { 14 | "package": "swift-log", 15 | "repositoryURL": "https://github.com/apple/swift-log.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "74d7b91ceebc85daf387ebb206003f78813f71aa", 19 | "version": "1.2.0" 20 | } 21 | }, 22 | { 23 | "package": "swift-nio", 24 | "repositoryURL": "https://github.com/apple/swift-nio.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "e876fb37410e0036b98b5361bb18e6854739572b", 28 | "version": "2.16.0" 29 | } 30 | }, 31 | { 32 | "package": "swift-nio-extras", 33 | "repositoryURL": "https://github.com/apple/swift-nio-extras.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "b4dbfacff47fb8d0f9e0a422d8d37935a9f10570", 37 | "version": "1.4.0" 38 | } 39 | }, 40 | { 41 | "package": "swift-nio-ssl", 42 | "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "ae213938e151964aa691f0e902462fbe06baeeb6", 46 | "version": "2.7.1" 47 | } 48 | } 49 | ] 50 | }, 51 | "version": 1 52 | } 53 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/FileMakerNIO.swift: -------------------------------------------------------------------------------- 1 | import AsyncHTTPClient 2 | import NIO 3 | import Logging 4 | 5 | public class FileMakerNIO { 6 | let client: Client 7 | let logger: Logger 8 | let configuration: FileMakerConfiguration 9 | 10 | var token: String? 11 | 12 | var apiBaseURL: String { 13 | "\(configuration.scheme)://\(configuration.hostname):\(configuration.port)/fmi/data/v1/databases/\(configuration.databaseName)/" 14 | } 15 | 16 | public init(configuration: FileMakerConfiguration, client: Client = HTTPClient.init(eventLoopGroupProvider: .createNew), logger: Logger) { 17 | self.configuration = configuration 18 | self.client = client 19 | self.logger = logger 20 | } 21 | 22 | func getToken() throws -> String { 23 | guard let token = self.token else { 24 | throw FileMakerNIOError(message: "Token does not exist") 25 | } 26 | return token 27 | } 28 | 29 | public func start(on eventLoop: EventLoop) -> EventLoopFuture { 30 | let authentication = FilemakerAuthentication(client: self.client, logger: self.logger, configuration: self.configuration, baseURL: self.apiBaseURL) 31 | return authentication.login(on: eventLoop).map { loginResponse in 32 | self.token = loginResponse.response.token 33 | } 34 | } 35 | 36 | public func stop(on eventLoop: EventLoop) -> EventLoopFuture { 37 | let authentication = FilemakerAuthentication(client: self.client, logger: self.logger, configuration: self.configuration, baseURL: self.apiBaseURL) 38 | let token: String 39 | do { 40 | token = try getToken() 41 | } catch { 42 | return eventLoop.makeFailedFuture(error) 43 | } 44 | return authentication.logout(token: token, on: eventLoop) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Operations/FileMakerAuthentication.swift: -------------------------------------------------------------------------------- 1 | import NIO 2 | import Foundation 3 | import Logging 4 | 5 | struct FilemakerAuthentication { 6 | 7 | let client: Client 8 | let logger: Logger 9 | let configuration: FileMakerConfiguration 10 | let baseURL: String 11 | 12 | func login(on eventLoop: EventLoop) -> EventLoopFuture { 13 | let url = "\(baseURL)sessions" 14 | logger.trace("FILEMAKERNIO - attempting login to \(url)") 15 | return client.sendRequest(to: url, method: .POST, data: EmptyRequest(), sessionToken: nil, basicAuth: .init(username: configuration.username, password: configuration.password), logger: self.logger, eventLoop: eventLoop).flatMapThrowing { response in 16 | guard response.status == .ok else { 17 | if response.status == .unauthorized { 18 | throw FileMakerNIOError(message: "The username or password was incorrect") 19 | } else { 20 | throw FileMakerNIOError(message: "There was an error logging in") 21 | } 22 | } 23 | guard let body = response.body else { 24 | throw FileMakerNIOError(message: "The login response contained no data") 25 | } 26 | let response = try JSONDecoder().decode(LoginResponse.self, from: body) 27 | self.logger.notice("FILEMAKERNIO - Login successful") 28 | return response 29 | } 30 | } 31 | 32 | func logout(token: String, on eventLoop: EventLoop) -> EventLoopFuture { 33 | let url = "\(baseURL)sessions/\(token)" 34 | return client.sendRequest(to: url, method: .DELETE, sessionToken: nil, logger: self.logger, eventLoop: eventLoop).flatMapThrowing { response in 35 | guard response.status == .ok else { 36 | throw FileMakerNIOError(message: "Failed to log out of database") 37 | } 38 | self.logger.info("FILEMAKERNIO - Log out successful") 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Client/HTTPClient+Requester.swift: -------------------------------------------------------------------------------- 1 | import AsyncHTTPClient 2 | import NIOHTTP1 3 | import NIO 4 | import Foundation 5 | import Logging 6 | 7 | extension HTTPClient: Client { 8 | 9 | public func sendRequest(to url: String, method: HTTPMethod, sessionToken: String?, logger: Logger, eventLoop: EventLoop) -> EventLoopFuture { 10 | executeRequest(to: url, method: method, sessionToken: sessionToken, logger: logger, eventLoop: eventLoop) 11 | } 12 | 13 | public func sendRequest(to url: String, method: HTTPMethod, data: T, sessionToken: String?, basicAuth: BasicAuthCredentials?, logger: Logger, eventLoop: EventLoop) -> EventLoopFuture where T : Encodable { 14 | let body: Body 15 | do { 16 | let bodyData = try JSONEncoder().encodeAsByteBuffer(data, allocator: ByteBufferAllocator()) 17 | let bodyString = String(decoding: bodyData.readableBytesView, as: UTF8.self) 18 | logger.trace("FILEMAKERNIO - Request will be: \(bodyString)") 19 | body = Body.byteBuffer(bodyData) 20 | } catch { 21 | return eventLoop.makeFailedFuture(error) 22 | } 23 | return executeRequest(to: url, method: method, sessionToken: sessionToken, basicAuth: basicAuth, body: body, logger: logger, eventLoop: eventLoop) 24 | } 25 | 26 | func executeRequest(to url: String, method: HTTPMethod, sessionToken: String?, basicAuth: BasicAuthCredentials? = nil, body: Body? = nil, logger: Logger, eventLoop: EventLoop) -> EventLoopFuture { 27 | logger.trace("FILEMAKERNIO - will send request to \(url)") 28 | var headers = HTTPHeaders() 29 | headers.add(name: "content-type", value: "application/json") 30 | if let token = sessionToken { 31 | headers.add(name: "authorization", value: "Bearer \(token)") 32 | } 33 | if let basicAuth = basicAuth { 34 | let authString = "\(basicAuth.username):\(basicAuth.password)" 35 | let encoded = Data(authString.utf8).base64EncodedString() 36 | headers.add(name: "authorization", value: "Basic \(encoded)") 37 | } 38 | let request: HTTPClient.Request 39 | do { 40 | request = try HTTPClient.Request(url: url, method: method, headers: headers, body: body) 41 | logger.trace("FILEMAKERNIO - Sending request \(request)") 42 | } catch { 43 | return eventLoop.makeFailedFuture(error) 44 | } 45 | let eventLoopPreference = EventLoopPreference.delegate(on: eventLoop) 46 | return self.execute(request: request, eventLoop: eventLoopPreference).map { response in 47 | logger.trace("FILEMAKERNIO - Received response \(response)") 48 | if let body = response.body { 49 | let bodyString = String(decoding: body.readableBytesView, as: UTF8.self) 50 | logger.trace("FILEMAKERNIO - Response body: \(bodyString)") 51 | } 52 | return response 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Operations/RecordOperations.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | 4 | public extension FileMakerNIO { 5 | 6 | var layoutsURL: String { 7 | "\(self.apiBaseURL)layouts/" 8 | } 9 | 10 | func createRecord(layout: String, data: T, on eventLoop: EventLoop) -> EventLoopFuture where T: FMIdentifiable { 11 | let url = "\(self.layoutsURL)\(layout)/records" 12 | let createData = CreateRecordRequest(fieldData: data) 13 | return self.performOperation(url: url, data: createData, type: CreateRecordResponse.self, on: eventLoop).map { response in 14 | data.modId = Int(response.modId) 15 | data.recordId = Int(response.recordId) 16 | return data 17 | } 18 | } 19 | 20 | func editRecord(_ id: Int, layout: String, updateData: T, on eventLoop: EventLoop) -> EventLoopFuture where T: Codable { 21 | let url = "\(self.layoutsURL)\(layout)/records/\(id)" 22 | return self.performOperation(url: url, data: updateData, type: EditRecordResponse.self, on: eventLoop) 23 | } 24 | 25 | func duplicateRecord(_ id: Int, layout: String, decodeTo type: T.Type, on eventLoop: EventLoop) -> EventLoopFuture where T: Encodable { 26 | let url = "\(self.layoutsURL)\(layout)/records/\(id)" 27 | return self.performOperation(url: url, data: EmptyRequest(), type: DuplicateRecordResponse.self, on: eventLoop) 28 | } 29 | 30 | func deleteRecord(_ id: Int, layout: String, on eventLoop: EventLoop) -> EventLoopFuture { 31 | let url = "\(self.layoutsURL)\(layout)/records/\(id)" 32 | return self.performOperation(url: url, type: DeleteRecordResponse.self, on: eventLoop).map { _ in } 33 | } 34 | 35 | func getRecord(_ id: Int, layout: String, decodeTo type: T.Type, on eventLoop: EventLoop) -> EventLoopFuture where T: FMIdentifiable { 36 | let url = "\(self.layoutsURL)\(layout)/records/\(id)" 37 | return self.performOperation(url: url, type: GetRecordResponse.self, on: eventLoop).flatMapThrowing { getRecordResponse in 38 | guard let record = getRecordResponse.data.first else { 39 | throw FileMakerNIOError(message: "Record does not exist when it should") 40 | } 41 | return record.completeModel() 42 | } 43 | } 44 | 45 | func getRecords(layout: String, decodeTo type: T.Type, offset: Int? = nil, limit: Int? = nil, on eventLoop: EventLoop) -> EventLoopFuture<[T]> where T: FMIdentifiable { 46 | var url = "\(self.layoutsURL)\(layout)/records" 47 | if offset != nil || limit != nil { 48 | url += "?" 49 | } 50 | if let offset = offset { 51 | url += "_offset=\(offset)" 52 | } 53 | if let limit = limit { 54 | if offset != nil { 55 | url += "&" 56 | } 57 | url += "_limit=\(limit)" 58 | } 59 | return self.performOperation(url: url, type: GetRangeOfRecordsResponse.self, on: eventLoop).map { getRangeOfRecordsResponse in 60 | getRangeOfRecordsResponse.data.map { $0.completeModel() } 61 | } 62 | } 63 | 64 | func findRecords(layout: String, payload:T, decodeTo type: R.Type, on eventLoop: EventLoop) -> EventLoopFuture<[R]> where T: Codable, R: FMIdentifiable { 65 | let url = "\(self.layoutsURL)\(layout)/_find" 66 | return self.performOperation(url: url, data: payload, type: FindRecordsResponse.self, on: eventLoop).map { findResponse in 67 | findResponse.data.map { $0.completeModel() } 68 | } 69 | } 70 | } 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /Sources/FileMakerNIO/Operations/Operations.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | import AsyncHTTPClient 4 | 5 | extension FileMakerNIO { 6 | 7 | internal func performOperation(url: String, data: T, type: R.Type, on eventLoop: EventLoop) -> EventLoopFuture where T: Encodable, R: CodableAction { 8 | let token: String 9 | do { 10 | token = try self.getToken() 11 | } catch { 12 | return eventLoop.makeFailedFuture(error) 13 | } 14 | return self.client.sendRequest(to: url, method: type.method, data: data, sessionToken: token, basicAuth: nil, logger: self.logger, eventLoop: eventLoop).flatMap { response in 15 | guard response.status != .unauthorized else { 16 | return self.start(on: eventLoop).flatMap { 17 | let updatedToken: String 18 | do { 19 | updatedToken = try self.getToken() 20 | } catch { 21 | return eventLoop.makeFailedFuture(error) 22 | } 23 | return self.client.sendRequest(to: url, method: type.method, data: data, sessionToken: updatedToken, basicAuth: nil, logger: self.logger, eventLoop: eventLoop).flatMapThrowing { response in 24 | try self.validateAndGetResponse(response, type: type) 25 | } 26 | } 27 | } 28 | do { 29 | let result = try self.validateAndGetResponse(response, type: type) 30 | return eventLoop.makeSucceededFuture(result) 31 | } catch { 32 | return eventLoop.makeFailedFuture(error) 33 | } 34 | } 35 | } 36 | 37 | internal func performOperation(url: String, type: R.Type, on eventLoop: EventLoop) -> EventLoopFuture where R: CodableAction { 38 | let token: String 39 | do { 40 | token = try self.getToken() 41 | } catch { 42 | return eventLoop.makeFailedFuture(error) 43 | } 44 | return self.client.sendRequest(to: url, method: type.method, sessionToken: token, logger: self.logger, eventLoop: eventLoop).flatMap { response in 45 | guard response.status != .unauthorized else { 46 | return self.start(on: eventLoop).flatMap { 47 | let updatedToken: String 48 | do { 49 | updatedToken = try self.getToken() 50 | } catch { 51 | return eventLoop.makeFailedFuture(error) 52 | } 53 | return self.client.sendRequest(to: url, method: type.method, sessionToken: updatedToken, logger: self.logger, eventLoop: eventLoop).flatMapThrowing { response in 54 | try self.validateAndGetResponse(response, type: type) 55 | } 56 | } 57 | } 58 | do { 59 | let result = try self.validateAndGetResponse(response, type: type) 60 | return eventLoop.makeSucceededFuture(result) 61 | } catch { 62 | return eventLoop.makeFailedFuture(error) 63 | } 64 | } 65 | } 66 | 67 | internal func validateAndGetResponse(_ response: HTTPClient.Response, type: T.Type) throws -> T where T: CodableAction { 68 | guard let body = response.body else { 69 | throw FileMakerNIOError(message: "The FileMaker response contained no data") 70 | } 71 | let fmResponse = try JSONDecoder().decode(FileMakerResponse.self, from: body) 72 | guard let message = fmResponse.messages.first else { 73 | throw FileMakerNIOError(message: "Invalid response from FileMaker") 74 | } 75 | guard message.code == "0" else { 76 | self.logger.error("FILEMAKERNIO - received error code \(message.code) from FileMaker with message \(message.message)") 77 | let filemakerError = FileMakerError(errorCode: message.code, message: message.message) 78 | throw filemakerError 79 | } 80 | let realResponse = try JSONDecoder().decode(FileMakerResponse.self, from: body) 81 | return realResponse.response 82 | } 83 | } 84 | --------------------------------------------------------------------------------