Skip to content

Communicate with FileMaker databases using the FileMaker Data API and Combine Publishers.

Notifications You must be signed in to change notification settings

pacific707/FMData

Repository files navigation

FMData

A comprehensive and declarative Library to use the FileMaker Data API. This library exposes the FileMaker Data API as Combine Publishers. This library takes advantage of the use of generics to keep things type safe and provide better debugging. All basic query and request structs are provided that match the data api. All you need to provide is your encodable response objects.

Swift Package Manager

NEW - async/awat

When using Swift 5.5 you can now convert any publisher to an async call. Just follow up your combine pipeline with .awaitSingleResult() to return an async Result<Output, FMRest.APIError>.

Index

Features

  • Combine Publishers
  • Token Management
  • Multi database or server
  • Type safe using generics
  • Support for ClarisID
  • Oath
  • External data source auth
  • Supports FileMaker Server 18+

Using FMData

Each method is called from the context of the main three objects Server, Database and Layout. You can also reach each object from the context of the child. Example for a layout called "contacts" you could reach the server object as shown: contacts.database.server.

Setting up

In these examples we are setting up a single server and working with one database. You could be using many servers, databases and layouts. The relation of which is a simple tree structure where a server can have many databases, a database can have many layouts. When you make a call from the context of the layout the call uses information from up the tree. It knows the server host from the server object. It gets the credentials stored in the database object etc.

You would likely be setting up the server, database and layouts in you apps model.

Initializing a server

We start by creating our server

let myServer = DataAPI.Server(host: "www.example.com")

Adding a database to a server

let myDatabase = myServer.addDatabase("databaseName")

if you will be using one database the whole time so you can initialize the database and server at once. Then you could just access the server from the database when needed.

let myDatabase = DataAPI.Server(host: "www.example.com").addDatabase("databaseName")

Add layouts to a database

Layouts need to be defined with a LayoutKey protocol

enum Layouts: String, LayoutKey {
    case myLayout
    case myOtherLayout
}
let myLayout = myDatabase.addLayout(LayoutSet.myLayout)

Again if you are only using one layout you can define all three at once and just access the server and database from the layout.

let myLayout = myServer("www.example.com).addDatabase("databaseName").addLayout(LayoutSet.myLayout)

Request Objects

Authentication Objects

Credentials

The credentials object is stored in the database object. It is initialized as basic, oAuth or Claris ID. On successful login it is replaced by a token. The credentials object is published so if you have a timer or something observing you can act on the change when it flips to invalid.

//Basic Auth
let credentials = Credentials(user: String, password: String)

//OAuth
let credentials = Credentials(oAuthId: String, oAuthIdentifier: String)

//ClarisId
let credentials = Credentials(clarisId: String)

FMDataSourceAuth

If you are connecting to a database with an external data source that needs separate authentication you can send an array of FMDataSourceAuth

//Basic Auth
let dataSourceAuth = FMDataSourceAuth(fmDataSource: [
                                        .init(
                                            database: "externalDb",
                                            credentials: .basic(
                                                user: "userName",
                                                password: "password"
                                            )
                                        )
])
//OAuth
let dataSourceAuth = FMDataSourceAuth(fmDataSource: [
                                        .init(
                                            database: "externalDb",
                                            credentials: .oAuth(
                                                requestId: "requestString",
                                                identifier: "identifierString"
                                            )
                                        )
])

Request Objects

Script Query

A script query can be pass with any Record request. The script will be processed on server and the result will be optionally be returned in the response object

let scriptQuery = ScriptQuery(
    script: String?,
    scriptParam: String?,
    scriptPreSort: String?,
    scriptPreSortParam: String?,
    scriptPreRequest: String?,
    scriptPreRequestParam: String?
)

Portal Query

A portal can be sent in with calls that return records. The portal array will have the name of the portal to return and optional limits and offsets for each portal

let portals = [
    PortalRequest(name: "portalName"),
    PortalRequest(
        name: "portalName2",
        limit: 30,
        offset: 10)
]

Sort Query

Requests that return multiple records can have a sort paramter. Its simply an array of the field name and order. Not providing the order will default to ascend.

let sort = [
    SortQuery(fieldName: "fieldName"),
    SortQuery(fieldName: "fieldName2", sortOrder: .descend)
]

Record Query

A record query can optionally be sent with get records requests to specify scripts to run layouts response portals and offsets etc.

let recordQuery = RecordQuery(
    script: ScriptQuery?,
    layoutResponse: String?,
    portal: [Portal]?,
    offset: String?,
    limit: String?,
    sortOrder: [SortQuery]?
)

Find Query

A Find query can specify field and values of the records you want to find on. You can also include scripts offsets etc like other calls. Only the basic query is required.

let find = FindQuery(
    query: [["fieldName" : "fieldValue"]],
    sort: [SortQuery]?,
    script: ScriptQuery?,
    offset: Int?,
    limit: Int?,
    layoutResponse: String?,
    portal: [Portal]?
)

Edit Record

An Edit records query should have the updated record you are editing. It can include portal data. Portals needs to be named. The object used to make this request should be the same as what is returned. The EditRecord object is used to update and create new objects.

//init for editing
let updated = EditRecord(
    editRecord: R,
    portalData: P?,
    modId: String?,
    script: ScriptQuery?,
    layoutResponse: String?)

//simple example - the RecordID of the record to edit is provided in the call
let updated = EditRecord(editRecord: myObject)

//init for creating
let updated = EditRecord(
    createRecord: R,
    portalData: P?,
    script: ScriptQuery?,
    layoutResponse: String?
)

//simple new record
let newRecord = EditRecord(createRecord: myObject)

Response Objects

Meta Data

Product Info

public struct ProductInfo: Decodable {
    public let buildDate: String
    public let name: String
    public let version: String
    public let dateFormat: String
    public let timeFormat: String
    public let timeStampFormat: String
}

Database Names

public struct DatabaseListResponse: Decodable {
    
    public let databases: [DatabaseName]
    
    public struct DatabaseName: Decodable {
        public let name: String
    }
    
}

Layout Names

Layout names are received as a list of enums. Each one being a Layout object or a folder recursively

public enum LayoutListItem: Decodable {
    
    case layout(layout: DataAPI.Layout)
    case folder(name: String, layouts: [LayoutListItem])
    
    public var name: String { get }
}

Script Names

Script names are received as a list of enums. Each one being a name or folder recursively

public enum ScriptItem: Decodable {
    
    case script(_ name: String)
    case folder(_ name: String, scripts: [ScriptItem])
    
    public var name: String { get }
}

Layout Metadata

public struct LayoutMetaData: Decodable {
    
    public let fieldMetaData: [FieldMetaData]
    public let portalMetaData: [String: [FieldMetaData]]
    public let valueLists: [ValueLists]?
    
    public struct FieldMetaData: Decodable {
        public let name: String
        public let type: FieldType
        public let displayType: DisplayType?
        public let result: CalcResult
        public let global: Bool
        public let autoEnter: Bool
        public let fourDigitYear: Bool
        public let maxRepeat: Int
        public let maxCharacters: Int?
        public let notEmpty: Bool
        public let numeric: Bool
        public let repetitions: Int?
        public let timeOfDay: Bool
        public let valueList: String?
        
        public enum FieldType: String, Decodable {
            case normal
            case calculation
            case summary
        }
        
        public enum DisplayType: String, Decodable {
            case editText
            case popupList
            case popupMenu
            case checkBox
            case radioButtons
            case selectionList
            case calendar
            case secureText
        }
        
        public enum CalcResult: String, Decodable {
            case text
            case number
            case date
            case time
            case timeStamp
            case container
            
        }
        
    }
    
    public struct ValueLists: Decodable {
        public let name: String
        public let type: String
        public let values: [Value]
        
        public struct Value: Decodable {
            public let value: String
            public let displayValue: String
        }
    }
    
}

Record Objects

Script Response

Any response where a script is called can have a scriptResponse.

public struct ScriptResponse: Decodable {
    public let scriptResult: String?
    public let scriptError: String?
    public let scriptErrorPreRequest: String?
    public let scriptResultPreRequest: String?
    public let scriptResultPreSort: String?
    public let scriptErrorPreSort: String?
    
}

Record Response

A record response has the generic object and generic portal object you specified. Both need to be decodable. In the often case where there is no portal records and EmptyPortal object can be used. If you want to have multi portals they need to be created like this

public struct ExamplePortal: Decodable {

// the name of these let variables are the names of the 
// portals or you can make a coding key for them.
    let portal1: [PortalObject1]
    let portal2: [PortalObject2]

    struct PortalObject1 : DataAPIPortalRecord {
        var recordId: String
        var modId: String
        ...
    }
    
    struct PortalObject2: DataAPIPortalRecord {
        var recordId: String
        var modId: String
        ...
    }
}

Here is the basic response record. For convenience you can supply the empty portal struct when no portal is needed.

public struct RecordResponse<R: Decodable, P:Decodable>: Decodable {
    
    public let data: [Record<R,P>]
    public let dataInfo: DataInfo
    public let scriptResponse: ScriptResponse?
    
    public struct Record<R: Decodable>: Decodable {
        public let fieldData: R
        public let portalData: P?
        public let modId: String
        public let recordId: String
        public let portalDataInfo: [PortalDataInfo]?
    }
    
    public struct DataInfo: Decodable {
        
        public let database: String
        public let layout: String
        public let table: String
        public let totalRecordCount: Int
        public let foundCount: Int
        public let returnedCount: Int
        
    }
    
    public struct PortalDataInfo: Decodable {
        
        public let database: String
        public let table: String
        public let foundCount: Int
        public let returnedCount: Int
        
    }
    
    public struct EmptyPortal: Decodable {
        
    }
}

Create Response

public struct CreateResponse: Decodable {
    public let recordId: String
    public let modId: String
    public let scriptResponse: ScriptResponse?
}

EditRecordResponse

public struct EditRecordResponse: Decodable {
    public let modId: String
    public let newPortalRecordInfo: CreatedPortalRecordInfo?
    public let scriptResponse: ScriptResponse?
    
    public struct CreatedPortalRecordInfo: Decodable {
        public let tableName: String
        public let recordId: String
        public let modId: String
    }
}

Publishers (endpoints)

A note about combine publishers

In the examples below we are going to look at just the publishers available. Then you can add your own operators and subscribers. The publishers return decoded objects. The compiler will infer the type when you deal with the result for the record publishers.

Metadata

Product Info

Function signature

func getProductInfo() -> AnyPublisher<ProductInfo, FMRest.APIError>

Example

myServer.getProductInfo()

Database Names

This is the only publisher in the server context that requires credentials. This is just to get the list of servers that are available using these credentials.

Function signature

func getDatabaseNames(credentials: DataAPI.Credentials) -> AnyPublisher<DataAPI.Database.DatabaseListResponse, FMRest.APIError>

Example

myServer.getDatabaseNames(credentials: .init(user:"username",password:"password"))

Layout Names

Function signature

func getLayoutNames() -> AnyPublisher<[LayoutListItem], FMRest.APIError>

Example

myDatabase.getLayoutNames()

Script Names

Function signature

func getScriptNames() -> AnyPublisher<[ScriptItem], FMRest.APIError>

Example

myDatabase.getScriptNames()

Layout Metadata

Function signature

func getLayoutMetadata(recordId: Int?) -> AnyPublisher<LayoutMetaData, FMRest.APIError>

Example

myLayout.getLayoutMetadata()

Authentication

Login

Function signature

func login(credentials: DataAPI.Credentials, dataSourceCredentials: DataAPI.FMDataSourceAuth? = nil) -> AnyPublisher<DataAPI.Credentials, FMRest.APIError>

Example

myDatabases.login(credentials: .init(user: "userName", password: "password"))

Log Out

Function signature

func logOut() -> AnyPublisher<FMRest.EmptyResponse, FMRest.APIError>

Example

myDatabase.logOut()

Validate Session

Only support for FileMaker Server v19+

Function signature

func validateSession(credentials: DataAPI.Credentials) -> AnyPublisher<FMRest.EmptyResponse, FMRest.APIError>

Example

myServer.validateSession(credentials: myDatabase.credentials)

Records

Get Records

Function signature

func getRecords<R: Decodable, P: Decodable>(recordQuery: DataAPI.RecordQuery? = nil) -> AnyPublisher<DataAPI.RecordResponse<R,P>, FMRest.APIError>

Example

myLayout.getRecords()

Create Record

A possible record example

let person = Person(firstName: "Bob", lastName: "Jones")
let myRecord = EditRecord(createRecord: person)

Function signature

func createRecord<R: Encodable, P: Encodable>(record: DataAPI.EditRecord<R,P>) -> AnyPublisher<DataAPI.CreateResponse, FMRest.APIError>

Example

myLayout.createRecord(record: myRecord)

Get Single Record By Id

Function signature

func getRecordById<R: Decodable, P: Decodable>(recordId: Int, recordQuery: DataAPI.RecordQuery? = nil)

Example

myLayout.getRecordById(recordId: 123)

Edit Record

Start with a record we want to edit. It can be any encodable object.

let editRequestObject = EditRecord(editRecord: someObjectThatsEdited)

Function signature

func editRecord<R: Encodable, P: Encodable>(record: DataAPI.EditRecord<R,P>, recordId: Int) -> AnyPublisher<DataAPI.EditRecordResponse, FMRest.APIError>

Example

myLayout.editRecord(record: editRequestObject, recordId: 50)

Delete Record

Function signature

func deleteRecord(recordId: Int, scriptQuery: DataAPI.ScriptQuery? = nil) -> AnyPublisher<DataAPI.ScriptResponse, FMRest.APIError>

Example

myLayout.deleteRecord(recordId: 26)

Duplicate Record

Function signature

func duplicateRecord(recordId: Int, scriptQuery: DataAPI.ScriptQuery? = nil) -> AnyPublisher<DataAPI.ScriptResponse, FMRest.APIError>

Example

myLayout.duplicateRecord(recordId: 123)

Find Records

In this example we have a layout called locations A query object

let cityQuery = FindQuery(query[["city":"Calgary"]])

Function signature

func findRecords<R: Decodable, P: Decodable>(query: DataAPI.FindQuery) -> AnyPublisher<DataAPI.RecordResponse<R,P>, FMRest.APIError>

Example

locations.findRecords(query: cityQuery)

Scripts

Execute Script

Function signature

func executeScript(script: String, scriptParam: String? = nil) -> AnyPublisher<DataAPI.ScriptResponse, FMRest.APIError>

Example

myLayout.executeScript(script: "scriptToRun")

Container

First we define the file for upload

let myFile = ContainerFile(fileName: "FileName", mimeType: "image/jpg", data: fileData )

Upload To Container Field

Function signature

func uploadToContainerField(fieldName: String, recordId: Int, repetition: Int?, modId: Int?, file: FMRest.ContainerFile) -> AnyPublisher<FMRest.EmptyResponse, FMRest.APIError>

Example

myLayout.uploadToContainerField(fieldName: "containerFileName", recordId: 12, file: myFile)

Upload To Container Field ( specific repetition )

Example

myLayout.uploadToContainerField(fieldName: "containerFileName", recordId: 12, repetition: 2, file: myFile)

Globals

Set Global Fields

Function signature

func setGlobalFields(globalFields: [String: String]) -> AnyPublisher<FMRest.EmptyResponse, FMRest.APIError>

Example

myDatabase.setGlobalFields(globalFields: ["fieldName"; "Value"])

About

Communicate with FileMaker databases using the FileMaker Data API and Combine Publishers.

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages