Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/starsite/swiftfm
SwiftFM is a Swift framework for the FileMaker Data API
https://github.com/starsite/swiftfm
api claris data-api filemaker ios ipados macos rest server swift swiftui xcode
Last synced: 4 days ago
JSON representation
SwiftFM is a Swift framework for the FileMaker Data API
- Host: GitHub
- URL: https://github.com/starsite/swiftfm
- Owner: starsite
- License: apache-2.0
- Created: 2018-09-12T04:58:31.000Z (over 6 years ago)
- Default Branch: main
- Last Pushed: 2024-03-01T20:34:30.000Z (10 months ago)
- Last Synced: 2024-12-30T20:27:23.062Z (5 days ago)
- Topics: api, claris, data-api, filemaker, ios, ipados, macos, rest, server, swift, swiftui, xcode
- Language: Swift
- Homepage:
- Size: 241 KB
- Stars: 59
- Watchers: 14
- Forks: 8
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Funding: FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
README
![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/starsite/SwiftFM) ![GitHub](https://img.shields.io/github/license/starsite/SwiftFM) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fstarsite%2FSwiftFM%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/starsite/SwiftFM) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fstarsite%2FSwiftFM%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/starsite/SwiftFM)
# SwiftFM
SwiftFM is a Swift Package for the FileMaker Data API. It uses modern Swift features like `async/await`, `Codable` type-safe returns, and has extensive support for `DocC`.
This `README.md` is aimed at Swift devs who want to use the Data API in their UIKit and SwiftUI projects. Each function shown below is paired with a code example.
SwiftFM is **in no way** related to the FIleMaker iOS App SDK.
---
### π³ How To Use
* Xcode -> File -> Add Packages
* `https://github.com/starsite/SwiftFM.git`
* **UIKit**: Set your enivronment in `applicationWillEnterForeground(_:)`
* **SwiftUI**: Set your enivronment in `MyApp.init()`
* Add an `import SwiftFM` statement
* Call `SwiftFM.newSession()` and get a token β¨
* Woot!---
### π How To Help
If you'd like to support the SwiftFM project, you can:
* Contribute socially, by giving SwiftFM a βοΈ on GitHub or telling other people about it
* Contribute [financially](https://paypal.me/starsite) (paypal.me/starsite)
* Hire me to build an iOS app for you or one of your FileMaker clients. π₯°---
### β Async/await
SwiftFM was rewritten last year to use `async/await`. This requires Swift 5.5 and iOS 15. If you need to compile for iOS 13 or 14, skip SPM and download the repo instead, and convert the `URLSession` calls using `withCheckedContinuation`. For more information on *that*, visit: [Swift by Sundell](https://wwdcbysundell.com/2021/wrapping-completion-handlers-into-async-apis/), [Hacking With Swift](https://www.hackingwithswift.com/quick-start/concurrency/how-to-use-continuations-to-convert-completion-handlers-into-async-functions), or watch Apple's WWDC 2021 [session](https://developer.apple.com/videos/play/wwdc2021/10132/) on the topic.
---
### π Table of Contents
* [`environment variables`](#environment-variables)
* [`newSession()`](#-new-session-function---token)
* [`validateSession(token:)`](#validate-session-function---bool)
* [`deleteSession(token:)`](#delete-session-function---escaping-bool)
* [`createRecord(layout:payload:token:)`](#-create-record-function---recordid)
* [`duplicateRecord(id:layout:token:)`](#duplicate-record-function---recordid)
* [`editRecord(id:layout:payload:token:)`](#edit-record-function---modid)
* [`deleteRecord(id:layout:token:)`](#-delete-record-function---bool)
* [`query(layout:payload:token:)`](#-query-function---record-datainfo)
* [`getRecords(layout:limit:sortField:ascending:portal:token:)`](#get-records-function---record-datainfo)
* [`getRecord(id:layout:token:)`](#get-record-function---record-datainfo)
* [`setGlobals(payload:token:)`](#set-globals-function---bool)
* [`getProductInfo()`](#get-product-info-function---productinfo)
* [`getDatabases()`](#get-databases-function---databases)
* [`getLayouts(token:)`](#get-layouts-function---layouts)
* [`getLayoutMetaData(layout:token:)`](#get-layout-metadata-function---response)
* [`getScripts(token:)`](#get-scripts-function---scripts)
* [`executeScript(script:parameter:layout:token:)`](#execute-script-function---bool)
* [`setContainer(recordId:layout:container:filePath:inferType:token:)`](#set-container-function---filename)---
### Environment Variables
For TESTING, you can set these with string literals. For PRODUCTION, you should be getting these values from elsewhere. DO NOT deploy apps with credentials visible in code. π΅
#### Example: Swift (UIKit)
Set your environment in `AppDelegate` inside `applicationWillEnterForeground(_:)`.
```swift
class AppDelegate: UIResponder, UIApplicationDelegate {
// ...
func applicationWillEnterForeground(_ application: UIApplication) {
let host = "my.host.com" //
let db = "my_database" //
// fetch these from elsewhere or prompt at launch
let user = "username" //
let pass = "password" //UserDefaults.standard.set(host, forKey: "fm-host")
UserDefaults.standard.set(db, forKey: "fm-db")
let str = "\(user):\(pass)"
if let auth = str.data(using: .utf8)?.base64EncodedString() {
UserDefaults.standard.set(auth, forKey: "fm-auth")
}
}
// ...
}
```#### Example: SwiftUI
Set your environment in `MyApp: App`. If you don't see an `init()` function, add one and finish it out like this.
```swift
@main
struct MyApp: App {
init() {
let host = "my.host.com" //
let db = "my_database" //
// fetch these from elsewhere or prompt at launch
let user = "username" //
let pass = "password" //UserDefaults.standard.set(host, forKey: "fm-host")
UserDefaults.standard.set(db, forKey: "fm-db")
let str = "\(user):\(pass)"
if let auth = str.data(using: .utf8)?.base64EncodedString() {
UserDefaults.standard.set(auth, forKey: "fm-auth")
}
}
var body: some Scene {
// ...
}
}
```---
### β¨ New Session (function) -> .token?
Returns an optional `token`.
If this fails due to an incorrect `Authorization`, the FileMaker Data API will return an error `code` and `message` to the console. All SwiftFM calls output a simple success or failure message.
```swift
func newSession() async -> String? {guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let auth = UserDefaults.standard.string(forKey: "fm-auth"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/sessions")else { return nil }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Basic \(auth)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMSession.self, from: data),
let message = result.messages.firstelse { return nil }
// return
switch message.code {
case "0":
guard let token = result.response.token else { return nil }UserDefaults.standard.set(token, forKey: "fm-token")
print("β¨ new token Β» \(token)")return token
default:
print(message)
return nil
}
}
```#### Example
```swift
if let token = await SwiftFM.newSession() {
print("β¨ new token Β» \(token)")
}
```---
### Validate Session (function) -> Bool
FileMaker Data API 19 or later. Returns a `Bool`. This function isn't all that useful on its own. But you can use it to wrap *other* calls to ensure they're fired with a valid `token`.
```swift
func validateSession(token: String) async -> Bool {guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/validateSession")else { return false }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMSession.self, from: data),
let message = result.messages.firstelse { return false }
// return
switch message.code {
case "0":
print("β valid token Β» \(token)")
return truedefault:
print(message)
return false
}
}
```#### Example
```swift
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let isValid = await SwiftFM.validateSession(token: token)switch isValid {
case true:
fetchArtists(token: token)case false:
if let newToken = await SwiftFM.newSession() {
fetchArtists(token: newToken)
}
}
```---
### Delete Session (function) -> @escaping Bool
Returns a `Bool`. For standard Swift (UIKit) apps, a good place to call this would be `applicationDidEnterBackground(_:)`. For SwiftUI apps, you should call it inside a `\.scenePhase.background` switch.
FileMaker's Data API has a 500-session limit, so managing session tokens will be important for larger deployments. If you don't delete your session token, it ~~will~~ *should* expire 15 minutes after the last API call. Probably. But you should clean up after yourself and not assume this will happen. π
```swift
func deleteSession(token: String, completion: @escaping (Bool) -> Void) {guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/sessions/\(token)")else { return }
var request = URLRequest(url: url)
request.httpMethod = "DELETE"URLSession.shared.dataTask(with: request) { data, resp, error in
guard let data = data, error == nil,
let result = try? JSONDecoder().decode(FMSession.self, from: data),
let message = result.messages.firstelse { return }
// return
switch message.code {
case "0":
UserDefaults.standard.set(nil, forKey: "fm-token")print("π₯ deleted token Β» \(token)")
completion(true)default:
print(message)
completion(false)
}}.resume()
}
```#### Example: Swift (UIKit)
```swift
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
// ...func applicationDidEnterBackground(_ application: UIApplication) {
if let token = UserDefaults.standard.string(forKey: "fm-token") {
SwiftFM.deleteSession(token: token) { _ in }
}
}
// ...
}
```#### Example: SwiftUI
```swift
@main
struct MyApp: App {var body: some Scene {
WindowGroup {
ContentView()
}
.onChange(of: scenePhase) { phase in
switch phase {
case .background:
DispatchQueue.global(qos: .background).async { // extra time
if let token = UserDefaults.standard.string(forKey: "fm-token") {
SwiftFM.deleteSession(token: token) { _ in }
}
}
default: break
}
}
} // .body
}
```---
### β¨ Create Record (function) -> .recordId?
Returns an optional `recordId`. This can be called with or without a payload. If you set a `nil` payload, a new empty record will be created. Either method will return a `recordId`. Set your payload with a `[String: Any]` object containing a `fieldData` key.
```swift
func createRecord(layout: String, payload: [String: Any]?, token: String) async -> String? {var fieldData: [String: Any] = ["fieldData": [:]] // nil payload
if let payload { // non-nil payload
fieldData = payload
}guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/records"),
let body = try? JSONSerialization.data(withJSONObject: fieldData)else { return nil }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = bodyguard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMRecord.self, from: data),
let message = result.messages.firstelse { return nil }
// return
switch message.code {
case "0":
guard let recordId = result.response.recordId else { return nil }print("β¨ new recordId: \(recordId)")
return recordIddefault:
print(message)
return nil
}
}
```#### Example
```swift
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let layout = "Artists"let payload = ["fieldData": [ // required key
"firstName": "Brian",
"lastName": "Hamm",
"email": "[email protected]"
]]if let recordId = await SwiftFM.createRecord(layout: layout, payload: payload, token: token) {
print("created record: \(recordId)")
}
```---
### Duplicate Record (function) -> .recordId?
FileMaker Data API 18 or later. Pretty simple call. Returns an optional `recordId` for the new record.
```swift
func duplicateRecord(id: Int, layout: String, token: String) async -> String? {guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/records/\(id)")else { return nil }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMRecord.self, from: data),
let message = result.messages.firstelse { return nil }
// return
switch message.code {
case "0":
guard let recordId = result.response.recordId else { return nil }print("β¨ new recordId: \(recordId)")
return recordIddefault:
print(message)
return nil
}
}
```#### Example
```swift
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let recid = 12345
let layout = "Artists"if let recordId = await SwiftFM.duplicateRecord(id: recid, layout: layout, token: token) {
print("new record: \(recordId)")
}
```------
### Edit Record (function) -> .modId?
Returns an optional `modId`. Pass a `[String: Any]` object with a `fieldData` key containing the fields you want to modify.
β οΈ If you include the `modId` value in your `payload` (from say, an earlier fetch), the record will only be modified if the `modId` matches the value on FileMaker Server. This ensures you're working with the current version of the record. If you do **not** pass a `modId`, your changes will be applied without this check.
Note: The FileMaker Data API does not pass back a modified record object for you to use. So you might want to refetch the updated record afterward with `getRecord(id:)`.
```swift
func editRecord(id: Int, layout: String, payload: [String: Any], token: String) async -> String? {guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/records/\(id)"),
let body = try? JSONSerialization.data(withJSONObject: payload)else { return nil }
var request = URLRequest(url: url)
request.httpMethod = "PATCH"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = bodyguard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMRecord.self, from: data),
let message = result.messages.firstelse { return nil }
// return
switch message.code {
case "0":
guard let modId = result.response.modId else { return nil }print("updated modId: \(modId)")
return modIddefault:
print(message)
return nil
}
}
```#### Example
```swift
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let recid = 12345
let layout = "Artists"let payload = ["fieldData": [
"address": "My updated address",
]]if let modId = await SwiftFM.editRecord(id: recid, layout: layout, payload: payload, token: token) {
print("updated modId: \(modId)")
}
```---
### π₯ Delete Record (function) -> Bool
Pretty self explanatory. Returns a `Bool`.
```swift
func deleteRecord(id: Int, layout: String, token: String) async -> Bool {guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/records/\(id)")else { return false }
var request = URLRequest(url: url)
request.httpMethod = "DELETE"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMBool.self, from: data),
let message = result.messages.firstelse { return false }
// return
switch message.code {
case "0":
print("deleted recordId: \(id)")
return truedefault:
print(message)
return false
}
}
```#### Example
β οΈ This is Swift, not FileMaker. Nothing will prevent this from firingβimmediately. Put some kind of confirmation view in your app.
```swift
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let recid = 12345
let layout = "Artists"let result = await SwiftFM.deleteRecord(id: recid, layout: layout, token: token)
if result == true {
print("deleted recordId \(recordId)")
}
```------
### π Query (function) -> ([record], .dataInfo)
Returns a `record` array and `dataInfo` response. This is our first function that returns a **tuple**. You can use either object (or both). The `dataInfo` object includes metadata about the request (database, layout, and table; as well as record count values for total, found, and returned). If you want to ignore `dataInfo`, you can assign it an underscore.
You can set your `payload` from the UI, or hardcode a query. Then pass it as a `[String: Any]` object with a `query` key.
```swift
func query(layout: String, payload: [String: Any], token: String) async throws -> (Data, FMResult.DataInfo) {
guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/_find"),
let body = try? JSONSerialization.data(withJSONObject: payload)
else { throw FMError.jsonSerialization }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = body
guard let (data, _) = try? await URLSession.shared.data(for: request),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let result = try? JSONDecoder().decode(FMResult.self, from: data), // .dataInfo
let response = json["response"] as? [String: Any],
let messages = json["messages"] as? [[String: Any]],
let message = messages[0]["message"] as? String,
let code = messages[0]["code"] as? String
else { throw FMError.sessionResponse }
// return
switch code {
case "0":
guard let data = response["data"] as? [[String: Any]],
let records = try? JSONSerialization.data(withJSONObject: data),
let dataInfo = result.response.dataInfoelse { throw FMError.jsonSerialization }
print("fetched \(dataInfo.foundCount) records")
return (records, dataInfo)
default:
print(message)
throw FMError.nonZeroCode
}
}
```#### Example
Note the difference in payload between an "or" request vs. an "and" request.
```swift
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let layout = "Artists"// find artists named Brian or Geoff
let payload = ["query": [
["firstName": "Brian"],
["firstName": "Geoff"]
]]// find artists named Brian in Dallas
let payload = ["query": [
["firstName": "Brian", "city": "Dallas"]
]]guard let (data, _) = try? await SwiftFM.query(layout: layout, payload: payload, token: token),
let records = try? JSONDecoder().decode([Artist].self, from: data)
else { return }self.artists = records // set @State data source
```---
### Get Records (function) -> ([record], .dataInfo)
Returns a `record` array and `dataInfo` response. All SwiftFM record fetching methods return a tuple.
```swift
func getRecords(layout: String,
limit: Int,
sortField: String,
ascending: Bool,
portal: String?,
token: String) async throws -> (Data, FMResult.DataInfo) {
// param str
let order = ascending ? "ascend" : "descend"
let sortJson = """
[{"fieldName":"\(sortField)","sortOrder":"\(order)"}]
"""
var portalJson = "[]" // nil portal
if let portal { // non-nil portal
portalJson = """
["\(portal)"]
"""
}
// encoding
guard let sortEnc = sortJson.urlEncoded,
let portalEnc = portalJson.urlEncoded,
let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/records/?_limit=\(limit)&_sort=\(sortEnc)&portal=\(portalEnc)")
else { throw FMError.urlEncoding }
// request
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
guard let (data, _) = try? await URLSession.shared.data(for: request),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let result = try? JSONDecoder().decode(FMResult.self, from: data), // .dataInfo
let response = json["response"] as? [String: Any],
let messages = json["messages"] as? [[String: Any]],
let message = messages[0]["message"] as? String,
let code = messages[0]["code"] as? String
else { throw FMError.sessionResponse }
// return
switch code {
case "0":
guard let data = response["data"] as? [[String: Any]],
let records = try? JSONSerialization.data(withJSONObject: data),
let dataInfo = result.response.dataInfoelse { throw FMError.jsonSerialization }
print("fetched \(dataInfo.foundCount) records")
return (records, dataInfo)
default:
print(message)
throw FMError.nonZeroCode
}
}
```#### Example (SwiftUI)
β¨ I'm including a complete SwiftUI example this time, showing the `model`, `view`, and a `fetchArtists(token:)` method. For those unfamiliar with SwiftUI, it's helpful to start in the middle of the example code and work your way out. Here's the gist:
There is a `.task` on `List` which will return data (async) from FileMaker. I'm using that to set our `@State var artists` array. When a `@State` property is modified, any view depending on it will be called again. In our case, this recalls `body`, refreshing `List` with our record data. Neat.
```swift
// model
struct Artist: Codable {
let recordId: String // β¨ useful as a \.keyPath in List views
let modId: String
let fieldData: FieldData
struct FieldData: Codable {
let name: String
}
}// view
struct ContentView: View {let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
// our data source
@State private var artists = [Artist]()
var body: some View {
NavigationView {
List(artists, id: \.recordId) { artist in
Text(artist.fieldData.name) // π₯° type-safe, Codable properties
}
.navigationTitle("Artists")
.task { // β <-- start here
let isValid = await SwiftFM.validateSession(token: token)switch isValid {
case true:
await fetchArtists(token: token)case false:
if let newToken = await SwiftFM.newSession() {
await fetchArtists(token: newToken)
}
}
} // .list
}
}
// ...// fetch 20 artists
func fetchArtists(token: String) async {guard let (data, _) = try? await SwiftFM.getRecords(layout: "Artists", limit: 20, sortField: "name", ascending: true, portal: nil, token: token)
let records = try? JSONDecoder().decode([Artist].self, from: data)
else { return }self.artists = records // sets our @State artists array π
}
// ...
}
```- - -
### Get Record (function) -> (record, .dataInfo)
Returns a `record` and `dataInfo` response.
```swift
func getRecord(id: Int, layout: String, token: String) async throws -> (Data, FMResult.DataInfo) {
guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/records/\(id)")
else { throw FMError.urlEncoding }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
guard let (data, _) = try? await URLSession.shared.data(for: request),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let result = try? JSONDecoder().decode(FMResult.self, from: data), // .dataInfo
let response = json["response"] as? [String: Any],
let messages = json["messages"] as? [[String: Any]],
let message = messages[0]["message"] as? String,
let code = messages[0]["code"] as? String
else { throw FMError.sessionResponse }
// return
switch code {
case "0":
guard let data = response["data"] as? [[String: Any]],
let data0 = data.first,
let record = try? JSONSerialization.data(withJSONObject: data0),
let dataInfo = result.response.dataInfoelse { throw FMError.jsonSerialization }
print("fetched recordId: \(id)")
return (record, dataInfo)
default:
print(message)
throw FMError.nonZeroCode
}
}
```#### Example
```swift
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let recid = 12345
let layout = "Artists"guard let (data, _) = try? await SwiftFM.getRecord(id: recid, layout: layout, token: token),
let record = try? JSONDecoder().decode(Artist.self, from: data)
else { return }self.artist = record
```- - -
### Set Globals (function) -> Bool
FileMaker Data API 18 or later. Returns a `Bool`. Make this call with a `[String: Any]` object containing a `globalFields` key.
```swift
func setGlobals(payload: [String: Any], token: String) async -> Bool {guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/globals"),
let body = try? JSONSerialization.data(withJSONObject: payload)else { return false }
var request = URLRequest(url: url)
request.httpMethod = "PATCH"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = bodyguard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMBool.self, from: data),
let message = result.messages.firstelse { return false }
// return
switch message.code {
case "0":
print("globals set")
return truedefault:
print(message)
return false
}
}
```#### Example
β οΈ Global fields must be set using fully qualified field names, ie. `table name::field name`. Also note that our result is a `Bool` and doesn't need to be unwrapped.
```swift
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""let payload = ["globalFields": [
"baseTable::gField": "newValue",
"baseTable::gField2": "newValue"
]]let result = await SwiftFM.setGlobals(payload: payload, token: token)
if result == true {
print("globals set")
}
```------
### Get Product Info (function) -> .productInfo?
FileMaker Data API 18 or later. Returns an optional `.productInfo` object.
```swift
func getProductInfo() async -> FMProduct.ProductInfo? {guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/productInfo")else { return nil }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMProduct.self, from: data),
let message = result.messages.firstelse { return nil }
// return
switch message.code {
case "0":
let info = result.response.productInfo
print("product: \(info.name) (\(info.version))")return info
default:
print(message)
return nil
}
}
```#### Example
This call doesn't require a token.
```swift
guard let info = await SwiftFM.getProductInfo() else { return }print(info.version) // properties for .name .buildDate, .dateFormat, .timeFormat, and .timeStampFormat
```------
### Get Databases (function) -> .databases?
FileMaker Data API 18 or later. Returns an optional array of `.database` objects.
```swift
func getDatabases() async -> [FMDatabases.Database]? {guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases")else { return nil }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMDatabases.self, from: data),
let message = result.messages.firstelse { return nil }
// return
switch message.code {
case "0":
let databases = result.response.databasesprint("\(databases.count) databases")
return databasesdefault:
print(message)
return nil
}
}
```#### Example
This call doesn't require a token.
```swift
guard let databases = await SwiftFM.getDatabases() else { return }print("\nDatabases:")
_ = databases.map{ print($0.name) } // like a .forEach, but shorter
```------
### Get Layouts (function) -> .layouts?
FileMaker Data API 18 or later. Returns an optional array of `.layout` objects.
```swift
func getLayouts(token: String) async -> [FMLayouts.Layout]? {guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts")else { return nil }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMLayouts.self, from: data),
let message = result.messages.firstelse { return nil }
// return
switch message.code {
case "0":
let layouts = result.response.layoutsprint("\(layouts.count) layouts")
return layoutsdefault:
print(message)
return nil
}
}
```#### Example
Many SwiftFM result types conform to `Comparable`. π₯° As such, you can use methods like `.sorted()`, `min()`, and `max()`.
```swift
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""guard let layouts = await SwiftFM.getLayouts(token: token) else { return }
// filter and sort folders
let folders = layouts.filter{ $0.isFolder == true }.sorted()folders.forEach { folder in
print("\n\(folder.name)")// tab indent folder contents
if let items = folder.folderLayoutNames?.sorted() {
items.forEach { item in
print("\t\(item.name)")
}
}
}
```------
### Get Layout Metadata (function) -> .response?
FileMaker Data API 18 or later. Returns an optional `.response` object, containing `.fields` and `.valueList` data. A `.portalMetaData` object is included as well, but will be unique to your FileMaker schema. So you'll need to model that yourself.
```swift
func getLayoutMetadata(layout: String, token: String) async -> FMLayoutMetaData.Response? {guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)")else { return nil }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMLayoutMetaData.self, from: data),
let message = result.messages.firstelse { return nil }
// return
switch message.code {
case "0":
if let fields = result.response.fieldMetaData {
print("\(fields.count) fields")
}if let valueLists = result.response.valueLists {
print("\(valueLists.count) value lists")
}return result.response
default:
print(message)
return nil
}
}
```#### Example
```swift
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let layout = "Artists"guard let result = await SwiftFM.getLayoutMetadata(layout: layout, token: token) else { return }
if let fields = result.fieldMetaData?.sorted() {
print("\nFields:")
_ = fields.map { print($0.name) }
}if let valueLists = result.valueLists?.sorted() {
print("\nValue Lists:")
_ = valueLists.map { print($0.name) }
}
```------
### Get Scripts (function) -> .scripts?
FileMaker Data API 18 or later. Returns an optional array of `.script` objects.
```swift
func getScripts(token: String) async -> [FMScripts.Script]? {guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/scripts")else { return nil }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMScripts.self, from: data),
let message = result.messages.firstelse { return nil }
// return
switch message.code {
case "0":
let scripts = result.response.scriptsprint("\(scripts.count) scripts")
return scriptsdefault:
print(message)
return nil
}
}
```#### Example
```swift
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""guard let scripts = await SwiftFM.getScripts(token: token) else { return }
// filter and sort folders
let folders = scripts.filter{ $0.isFolder == true }.sorted()folders.forEach { folder in
print("\n\(folder.name)")// tab indent folder contents
if let scripts = folder.folderScriptNames?.sorted() {
scripts.forEach { item in
print("\t\(item.name)")
}
}
}
```------
### Execute Script (function) -> Bool
Returns a `Bool`.
```swift
func executeScript(script: String, parameter: String?, layout: String, token: String) async -> Bool {// parameter
var param = "" // nil parameterif let parameter { // non-nil parameter
param = parameter
}// encoded
guard let scriptEnc = script.urlEncoded, // StringExtension.swift
let paramEnc = param.urlEncodedelse { return false }
// url
guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/script/\(scriptEnc)?script.param=\(paramEnc)")else { return false }
// request
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "Content-Type")guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMBool.self, from: data),
let message = result.messages.firstelse { return false }
// return
switch message.code {
case "0":print("fired script: \(script)")
return truedefault:
print(message)
return false
}
}
```#### Example
`Script` and `parameter` values are `.urlEncoded`, so spaces and such are ok.
```swift
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let script = "test script"
let layout = "Artists"let result = await SwiftFM.executeScript(script: script, parameter: nil, layout: layout, token: token)
if result == true {
print("fired script: \(script)")
}
```------
### Set Container (function) -> fileName?
```swift
func setContainer(recordId: Int,
layout: String,
container: String,
filePath: URL,
inferType: Bool,
token: String) async -> String? {guard let host = UserDefaults.standard.string(forKey: "fm-host"),
let db = UserDefaults.standard.string(forKey: "fm-db"),
let url = URL(string: "https://\(host)/fmi/data/vLatest/databases/\(db)/layouts/\(layout)/records/\(recordId)/containers/\(container)")else { return nil }
// request
let boundary = UUID().uuidStringvar request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")// file data
guard let fileData = try? Data(contentsOf: filePath) else { return nil }
let mimeType = inferType ? fileData.mimeType : "application/octet-stream" // DataExtension.swift// body
let br = "\r\n"
let fileName = filePath.lastPathComponent // β¨ <-- method returnvar httpBody = Data()
httpBody.append("\(br)--\(boundary)\(br)")
httpBody.append("Content-Disposition: form-data; name=upload; filename=\(fileName)\(br)")
httpBody.append("Content-Type: \(mimeType)\(br)\(br)")
httpBody.append(fileData)
httpBody.append("\(br)--\(boundary)--\(br)")request.setValue(String(httpBody.count), forHTTPHeaderField: "Content-Length")
request.httpBody = httpBody// session
guard let (data, _) = try? await URLSession.shared.data(for: request),
let result = try? JSONDecoder().decode(FMBool.self, from: data),
let message = result.messages.firstelse { return nil }
// return
switch message.code {
case "0":
print("container set: \(fileName)")
return fileNamedefault:
print(message)
return nil
}
}
```#### Example
An `inferType` of `true` will use `DataExtension.swift` (extensions folder) to attempt to set the mime-type automatically. If you don't want this behavior, set `inferType` to `false`, which assigns a default mime-type of "application/octet-stream".
```swift
let token = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let recid = 12345
let layout = "Artists"
let field = "headshot"guard let url = URL(string: "http://starsite.co/brian_memoji.png"),
let fileName = await SwiftFM.setContainer(recordId: recid,
layout: layout,
container: field,
filePath: url,
inferType: true,
token: token)
else { return }print("container set: \(fileName)")
```------
Starsite Labs π