alt text

GitHub release (latest SemVer) GitHub

SwiftFM

SwiftFM 是一个用于 FileMaker Data API 的 Swift 包。它使用现代 Swift 特性,例如 async/awaitCodable 类型安全返回,并对 DocC 提供广泛支持。

README.md 旨在帮助希望在 UIKit 和 SwiftUI 项目中使用 Data API 的 Swift 开发者。下面显示的每个函数都配有代码示例。

SwiftFM 与 FileMaker iOS App SDK 没有任何关系。


🗳 如何使用


🖐 如何帮助

如果您想支持 SwiftFM 项目,您可以


✅ Async/await

SwiftFM 去年被重写以使用 async/await。这需要 Swift 5.5 和 iOS 15。如果您需要为 iOS 13 或 14 编译,请跳过 SPM 并改为下载 repo,并使用 withCheckedContinuation 转换 URLSession 调用。有关这方面的更多信息,请访问:Swift by SundellHacking With Swift,或观看 Apple 的 WWDC 2021 关于此主题的会议


📔 目录


环境变量

对于测试,您可以使用字符串字面量设置这些变量。对于生产环境,您应该从其他地方获取这些值。请勿部署在代码中可见凭据的应用程序。😵

示例:Swift (UIKit)

AppDelegateapplicationWillEnterForeground(_:) 中设置您的环境。

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")
        }
    }
    
    // ...
}

示例:SwiftUI

MyApp: App 中设置您的环境。如果您没有看到 init() 函数,请添加一个并像这样完成它。

@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 {
        // ...
    }
}

✨ 新会话(函数) -> .token?

返回一个可选的 token

如果由于不正确的 Authorization 导致此操作失败,FileMaker Data API 将向控制台返回错误 codemessage。所有 SwiftFM 调用都会输出一个简单的成功或失败消息。

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.first

    else { 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
    }
}

示例

if let token = await SwiftFM.newSession() {
    print("✨ new token » \(token)")
}

验证会话(函数) -> Bool

FileMaker Data API 19 或更高版本。返回 Bool。此函数本身并不是非常有用。但是您可以使用它来包装其他调用,以确保它们使用有效的 token 触发。

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.first

    else { return false }

    // return
    switch message.code {
    case "0":
        print("✅ valid token » \(token)")
        return true

    default:
        print(message)
        return false
    }
}

示例

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)
    }       
}

删除会话(函数) -> @escaping Bool

返回 Bool。对于标准 Swift (UIKit) 应用程序,调用此函数的理想位置是 applicationDidEnterBackground(_:)。对于 SwiftUI 应用程序,您应该在 \.scenePhase.background 开关内调用它。

FileMaker 的 Data API 有 500 个会话的限制,因此管理会话令牌对于大型部署非常重要。如果您不删除会话令牌,它应该在上次 API 调用后 15 分钟过期。可能吧。但是您应该自己清理,而不要假设会发生这种情况。🙂

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.first

        else { 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()
}

示例:Swift (UIKit)

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
// ...

    func applicationDidEnterBackground(_ application: UIApplication) {
        if let token = UserDefaults.standard.string(forKey: "fm-token") {
            SwiftFM.deleteSession(token: token) { _ in }
        }
    }
    // ...
}

示例:SwiftUI

@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
}

✨ 创建记录(函数) -> .recordId?

返回一个可选的 recordId。可以在有或没有 payload 的情况下调用此函数。如果您设置 nil payload,将创建一个新的空记录。两种方法都将返回一个 recordId。使用包含 fieldData 键的 [String: Any] 对象设置您的 payload。

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 = body

    guard   let (data, _) = try? await URLSession.shared.data(for: request),
            let result    = try? JSONDecoder().decode(FMRecord.self, from: data),
            let message   = result.messages.first

    else { return nil }

    // return
    switch message.code {
    case "0":
        guard let recordId = result.response.recordId else { return nil }

        print("✨ new recordId: \(recordId)")
        return recordId

    default:
        print(message)
        return nil
    }
}

示例

let token  = UserDefaults.standard.string(forKey: "fm-token") ?? ""
let layout = "Artists"

let payload = ["fieldData": [  // required key
    "firstName": "Brian",
    "lastName": "Hamm",
    "email": "hello@starsite.co"
]]

if let recordId = await SwiftFM.createRecord(layout: layout, payload: payload, token: token) {
    print("created record: \(recordId)")
}

复制记录(函数) -> .recordId?

FileMaker Data API 18 或更高版本。非常简单的调用。为新记录返回一个可选的 recordId

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.first

    else { return nil }

    // return
    switch message.code {
    case "0":
        guard let recordId = result.response.recordId else { return nil }

        print("✨ new recordId: \(recordId)")
        return recordId

    default:
        print(message)
        return nil
    }
}

示例

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)")
}

编辑记录(函数) -> .modId?

返回一个可选的 modId。传递一个带有 fieldData 键的 [String: Any] 对象,其中包含您要修改的字段。

⚠️如果您在您的 payload 中包含 modId 值(例如,从先前的 fetch 中获得),则仅当 modId 与 FileMaker Server 上的值匹配时,记录才会被修改。这确保您正在使用记录的当前版本。如果您传递 modId,您的更改将在没有此检查的情况下应用。

注意:FileMaker Data API 不会传回修改后的记录对象供您使用。因此,您可能需要在之后使用 getRecord(id:) 重新获取更新的记录。

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 = body

    guard   let (data, _) = try? await URLSession.shared.data(for: request),
            let result    = try? JSONDecoder().decode(FMRecord.self, from: data),
            let message   = result.messages.first

    else { return nil }

    // return
    switch message.code {
    case "0":
        guard let modId = result.response.modId else { return nil }

        print("updated modId: \(modId)")
        return modId

    default:
        print(message)
        return nil
    }
}

示例

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)")
}

🔥 删除记录(函数) -> Bool

非常不言自明。返回 Bool

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.first

    else { return false }

    // return
    switch message.code {
    case "0":
        print("deleted recordId: \(id)")
        return true

    default:
        print(message)
        return false
    }
}

示例

⚠️这是 Swift,而不是 FileMaker。没有什么可以阻止它立即触发。在您的应用程序中放置某种确认视图。

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)")
}

🔍 查询(函数) -> ([record], .dataInfo)

返回一个 record 数组和 dataInfo 响应。这是我们的第一个返回元组的函数。您可以使用任一对象(或两者)。dataInfo 对象包括有关请求的元数据(数据库、布局和表;以及总数、找到和返回的记录计数)。如果您想忽略 dataInfo,您可以将其分配给下划线。

您可以从 UI 设置您的 payload,或硬编码查询。然后将其作为带有 query 键的 [String: Any] 对象传递。

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.dataInfo

        else { throw FMError.jsonSerialization }
        
        print("fetched \(dataInfo.foundCount) records")
        return (records, dataInfo)
        
    default:
        print(message)
        throw FMError.nonZeroCode
    }
}

示例

请注意“或”请求与“与”请求之间 payload 的差异。

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

获取记录(函数) -> ([record], .dataInfo)

返回一个 record 数组和 dataInfo 响应。所有 SwiftFM 记录获取方法都返回一个元组。

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.dataInfo

        else { throw FMError.jsonSerialization }
        
        print("fetched \(dataInfo.foundCount) records")
        return (records, dataInfo)
        
    default:
        print(message)
        throw FMError.nonZeroCode
    }
}

示例 (SwiftUI)

✨ 这次我包含了一个完整的 SwiftUI 示例,展示了 modelviewfetchArtists(token:) 方法。对于那些不熟悉 SwiftUI 的人,从示例代码的中间开始,然后向外展开会很有帮助。这是要点

List 上有一个 .task,它将从 FileMaker 返回数据(异步)。我正在使用它来设置我们的 @State var artists 数组。当 @State 属性被修改时,任何依赖于它的视图都将再次被调用。在我们的例子中,这将重新调用 body,使用我们的记录数据刷新 List。真棒。

// 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 👆
    }
    // ...
}

获取记录(函数) -> (record, .dataInfo)

返回一个 recorddataInfo 响应。

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.dataInfo

        else { throw FMError.jsonSerialization }
        
        print("fetched recordId: \(id)")
        return (record, dataInfo)
        
    default:
        print(message)
        throw FMError.nonZeroCode
    }
}

示例

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

设置全局(函数) -> Bool

FileMaker Data API 18 或更高版本。返回 Bool。使用包含 globalFields 键的 [String: Any] 对象进行此调用。

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 = body

    guard   let (data, _) = try? await URLSession.shared.data(for: request),
            let result    = try? JSONDecoder().decode(FMBool.self, from: data),
            let message   = result.messages.first

    else { return false }

    // return
    switch message.code {
    case "0":
        print("globals set")
        return true

    default:
        print(message)
        return false
    }
}

示例

⚠️全局字段必须使用完全限定的字段名称设置,即 table name::field name。另请注意,我们的结果是 Bool,不需要解包。

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")
}

获取产品信息(函数) -> .productInfo?

FileMaker Data API 18 或更高版本。返回一个可选的 .productInfo 对象。

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.first

    else { 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
    }
}

示例

此调用不需要令牌。

guard let info = await SwiftFM.getProductInfo() else { return }

print(info.version)  // properties for .name .buildDate, .dateFormat, .timeFormat, and .timeStampFormat

获取数据库(函数) -> .databases?

FileMaker Data API 18 或更高版本。返回 .database 对象的可选数组。

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.first

    else { return nil }

    // return
    switch message.code {
    case "0":
        let databases = result.response.databases

        print("\(databases.count) databases")
        return databases

    default:
        print(message)
        return nil
    }
}

示例

此调用不需要令牌。

guard let databases = await SwiftFM.getDatabases() else { return }

print("\nDatabases:")
_ = databases.map{ print($0.name) }  // like a .forEach, but shorter

获取布局(函数) -> .layouts?

FileMaker Data API 18 或更高版本。返回 .layout 对象的可选数组。

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.first

    else { return nil }

    // return
    switch message.code {
    case "0":
        let layouts = result.response.layouts

        print("\(layouts.count) layouts")
        return layouts

    default:
        print(message)
        return nil
    }
}

示例

许多 SwiftFM 结果类型都符合 Comparable。🥰 因此,您可以使用 .sorted()min()max() 等方法。

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)")
        }
    }
}

获取布局元数据(函数) -> .response?

FileMaker Data API 18 或更高版本。返回一个可选的 .response 对象,其中包含 .fields.valueList 数据。还包含一个 .portalMetaData 对象,但它对于您的 FileMaker 架构是唯一的。因此,您需要自己建模。

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.first

    else { 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
    }
}

示例

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) }
}

获取脚本(函数) -> .scripts?

FileMaker Data API 18 或更高版本。返回 .script 对象的可选数组。

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.first

    else { return nil }

    // return
    switch message.code {
    case "0":
        let scripts = result.response.scripts

        print("\(scripts.count) scripts")
        return scripts

    default:
        print(message)
        return nil
    }
}

示例

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)")
        }
    }
}

执行脚本(函数) -> Bool

返回 Bool

func executeScript(script: String, parameter: String?, layout: String, token: String) async -> Bool {

    // parameter
    var param = ""  // nil parameter

    if let parameter {  // non-nil parameter
        param = parameter
    }

    // encoded
    guard   let scriptEnc = script.urlEncoded,  // StringExtension.swift
            let paramEnc  = param.urlEncoded

    else { 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.first

    else { return false }

    // return
    switch message.code {
    case "0":

        print("fired script: \(script)")
        return true

    default:
        print(message)
        return false
    }
}

示例

Scriptparameter 值是 .urlEncoded 的,因此空格等是可以的。

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)")
}

设置容器(函数) -> fileName?

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().uuidString

    var 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 return

    var 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.first

    else { return nil }

    // return
    switch message.code {
    case "0":
        print("container set: \(fileName)")
        return fileName

    default:
        print(message)
        return nil
    }
}

示例

inferTypetrue 将使用 DataExtension.swift(extensions 文件夹)尝试自动设置 mime 类型。如果您不想要此行为,请将 inferType 设置为 false,这将分配默认的 mime 类型“application/octet-stream”。

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 😘