FileMaker
The FileMaker Server connector provides access to FileMaker Server databases using the XML Web publishing interface. It allows you to search for, add, update and delete records in a hosted FileMaker database.
System Requirements
This module uses libxml2 and libcurl along with the Perfect-XML and Perfect-CURL packages.
macOS
macOS currently includes suitable versions of the dependent libraries and no manual installation is necessary.
Linux
Install the libcurl4-openssl-dev & libxml2-dev packages through apt.
sudo apt-get install libcurl4-openssl-dev libxml2-dev
Setup
Add the "Perfect-FileMaker" project as a dependency in your Package.swift file:
.Package( url:"https://github.com/PerfectlySoft/Perfect-FileMaker.git", majorVersion: 3 )
Import
In any of the source files where you intend to use with this module, add the following import:
import PerfectFileMaker
Overview
The main objects that you will be working with when accessing FileMaker databases are described here.
struct FileMakerServer
This is the main interface to a FileMaker Server. Before accessing any database you will need to create an instance of this struct and provide a server host name or IP address, a port, and a valid username and password for the server. If the given port is 443 then all requests will be encrypted HTTPS.
Once the server connection is instantiated you can perform four different operations. These are:
- list available databases
- list a database's layouts
- list a layout's fields
- perform a query
In all cases you provide a callback and the operation occurs asynchronously. When the operation has completed the callback is called and given a closure which, when executed, will either return the operation response object or throw an error. The type of the operation response object will differ depending on the type of operation. The exception error object provides access to the error code and accompanying message string. It will be of the following type:
public enum FMPError: Error { /// An error code and message. case serverError(Int, String) }
The relevant portions of the FileMakerServer struct are defined as follows:
/// A connection to a FileMaker Server instance. /// Initialize using a host, port, username and password. public struct FileMakerServer { /// Initialize using a host, port, username and password. public init(host: String, port: Int, userName: String, password: String) /// Retrieve the list of database hosted by the server. public func databaseNames(completion: @escaping (() throws -> [String]) -> ()) /// Retrieve the list of layouts for a particular database. public func layoutNames(database: String, completion: @escaping (() throws -> [String]) -> ()) /// Get a database's layout information. Includes all field and portal names. public func layoutInfo(database: String, layout: String, completion: @escaping (() throws -> FMPLayoutInfo) -> ()) /// Perform a query and provide any resulting data. public func query(_ query: FMPQuery, completion: @escaping (() throws -> FMPResultSet) -> ()) }
When listing database or layout names, the response object will simply be an array of strings. When retrieving layout information, the response object will be a FMPLayoutInfo object. When performing a query the response object will be a FMPResultSet.
struct FMPLayoutInfo
FMPLayoutInfo is defined as follows:
/// Represents meta-information about a particular layout. public struct FMPLayoutInfo { /// Each field or related set as a list. public let fields: [FMPMetaDataItem] /// Each field or related set keyed by name. public let fieldsByName: [String:FMPFieldType] }
This object contains all of the information pertaining to the fields on a layout. This includes the field names and types and also indicates which portals/relations are on the layout and which fields are contained within them.
Field information is provided in two different ways. The first is represented by an array of FMPMetaDataItems. This enum value indicates if the item is a regular field or a portal/relation. The second is a dictionary containing the full field name and the field type. If the field is in a relation it will be named with the standard FileMaker PortalName::FieldName syntax.
What follows are the definitions for FMPMetaDataItem, FMPFieldDefinition & FMPFieldType.
/// Represents either an individual field definition or a related (portal) definition. public enum FMPMetaDataItem { /// An individual field. case fieldDefinition(FMPFieldDefinition) /// A related set. Indicates the portal name and its contained fields. case relatedSetDefinition(String, [FMPFieldDefinition]) }
/// A FileMaker field definition. Indicates a field name and type. public struct FMPFieldDefinition { /// The field name. public let name: String /// The field type. public let type: FMPFieldType }
/// One of the possible FileMaker field types. public enum FMPFieldType { /// A text field. case text /// A numeric field. case number /// A container field. case container /// A date field. case date /// A time field. case time /// A timestamp field. case timestamp }
struct FMPResultSet
An FMPResultSet object contains the result of a FileMaker query. This includes database & layout meta information, as well as the number of records which were found in total and a each record in the found set.
/// The result set produced by a query. public struct FMPResultSet { /// Database meta-info. public let databaseInfo: FMPDatabaseInfo /// Layout meta-info. public let layoutInfo: FMPLayoutInfo /// The number of records found by the query. public let foundCount: Int /// The list of records produced by the query. public let records: [FMPRecord] }
In addition to the previously described FMPLayoutInfo struct, a result set also contains the following bits of database related information:
/// Meta-information for a database. public struct FMPDatabaseInfo { /// The date format indicated by the server. public let dateFormat: String /// The time format indicated by the server. public let timeFormat: String /// The timestamp format indicated by the server. public let timeStampFormat: String /// The total number of records in the database. public let recordCount: Int }
The found record data is accessed through the FMPResultSet.records
property. This array of FMPRecords will contain one element for each returned record.
/// An individual result set record. public struct FMPRecord { /// A type of record item. public enum RecordItem { /// An individual field. case field(String, FMPFieldValue) /// A related set containing a list of related records. case relatedSet(String, [FMPRecord]) } /// The record id. public let recordId: Int /// The contained record items keyed by name. public let elements: [String:RecordItem] }
A record holds each field or portal item keyed by name in its elements
property. The values in this dictionary are FMPRecord.RecordItem
enum objects. These objects indicate if the item is a regular field, in which case its name and value can be directly accessed, or if the item is a related record set. If the item is a related record set then the portal name and an array of nested records are provided.
In either case a field's value is represented through the FMPFieldValue struct.
/// A returned FileMaker field value. public enum FMPFieldValue: CustomStringConvertible { /// A text field. case text(String) /// A numeric field. case number(Double) /// A container field. case container(String) /// A date field. case date(String) /// A time field. case time(String) /// A timestamp field. case timestamp(String) }
Queries
The FileMakerServer.query
function lets you search for sets of records or manipulate individual records. You specify a query using a FMPQuery struct. This struct is then formatted as a FileMaker XML query string and sent to the FileMaker Server. The server returns its response as XML. This XML is parsed and converted to a FMPResultSet object.
The query strings generated by the FMPQuery object correspond to what's defined in the "FileMaker® Server 12 Custom Web Publishing with XML" document. PDF Doc.
A query is created by instantiating a FMPQuery struct and then adding options to it. Each option add will return a new modified object. In this manner options can be "chained" to construct the desired final query object. The query object is then given to the FileMakerServer.query
function and the results are generated.
FMPQuery is as follows:
/// An individual query & database action. public struct FMPQuery: CustomStringConvertible { /// Initialize with a database name, layout name & database action. public init(database: String, layout: String, action: FMPAction) /// Sets the record id and returns the adjusted query. public func recordId(_ recordId: Int) -> FMPQuery /// Adds the query fields and returns the adjusted query. public func queryFields(_ queryFields: [FMPQueryFieldGroup]) -> FMPQuery /// Adds the query fields and returns the adjusted query. public func queryFields(_ queryFields: [FMPQueryField]) -> FMPQuery /// Adds the sort fields and returns the adjusted query. public func sortFields(_ sortFields: [FMPSortField]) -> FMPQuery /// Adds the indicated pre-sort scripts and returns the adjusted query. public func preSortScripts(_ preSortScripts: [String]) -> FMPQuery /// Adds the indicated pre-find scripts and returns the adjusted query. public func preFindScripts(_ preFindScripts: [String]) -> FMPQuery /// Adds the indicated post-find scripts and returns the adjusted query. public func postFindScripts(_ postFindScripts: [String]) -> FMPQuery /// Sets the response layout and returns the adjusted query. public func responseLayout(_ responseLayout: String) -> FMPQuery /// Adds response fields and returns the adjusted query. public func responseFields(_ responseFields: [String]) -> FMPQuery /// Sets the maximum records to fetch and returns the adjusted query. public func maxRecords(_ maxRecords: Int) -> FMPQuery /// Sets the number of records to skip in the found set and returns the adjusted query. public func skipRecords(_ skipRecords: Int) -> FMPQuery /// Returns the formulated query string. /// Useful for debugging purposes. public var queryString: String }
A FMPQuery is first instantiated with a database name, layout name and action. The possible actions are:
/// A database action. public enum FMPAction: CustomStringConvertible { /// Perform a search given the current query. case find /// Find all records in the database. case findAll /// Find and retrieve a random record. case findAny /// Create a new record given the current query data. case new /// Edit (update) the record indicated by the record id with the current query fields/values. case edit /// Delete the record indicated by the current record id. case delete /// Duplicate the record indicated by the current record id. case duplicate }
Many of the FMPQuery functions accepts Strings or Ints and are self-explanatory. Any function which accepts an array can be called multiple times and the new values will be appended to the existing values.
When performing an .edit
, .delete
or .duplicate
action a record id must be set by calling FMPQuery.recordId(_ recordId: Int)
. Setting a record id when performing a .find
will retrieve only the indicated record.
It is possible to search on one layout but return fields from another. Call FMPQuery.responseLayout(_ responseLayout: String)
to set the response layout. By default the layout which is searched on will be the response layout.
It is possible and advisable, for performance reasons, to return only the minimum of required fields in a result. The names of the fields you want returned in the result can be set through the FMPQuery.responseFields(_ responseFields: [String])
function.
Sorting and the usage of query fields are detailed below.
Sorting
Records in a result set can be returned sorted by indicating one or more sort fields and sort orders. This is accomplished by calling the FMPQuery.sortFields
function and passing the desired fields and orders as an array of FMPSortField objects. FMPSortField uses the FMPSortOrder enum to indicate the sort order. Both are defined as follows:
/// A record sort order. public enum FMPSortOrder: CustomStringConvertible { /// Sort the records by the indicated field in ascending order. case ascending /// Sort the records by the indicated field in descending order. case descending /// Sort the records by the indicated field in a custom order. case custom } /// A sort field indicator. public struct FMPSortField { /// The name of the field on which to sort. public let name: String /// A field sort order. public let order: FMPSortOrder /// Initialize with a field name and sort order. public init(name: String, order: FMPSortOrder) /// Initialize with a field name using the default FMPSortOrder.ascending sort order. public init(name: String) }
Query Fields
Query fields are added to a FMPQuery to indicate either fields which should be modified, in the case of an .edit
action or fields which should be searched on, in the case of a .find
action. Query fields hold a field name along with a corresponding value. In the case of the .find
action, query fields also hold a field level operator. These operators represent a relation of a field to the indicated value. For example, a field operator could indicate that a field's contents should be greater-than-or-equal to the value when performing a search. The default field level operator is begins-with (as is standard for FileMaker database searches).
Individual query fields are represented by FMPQueryField objects. Field level operators are represented by FMPFieldOp. These are defined as follows:
/// An individual field search operator. public enum FMPFieldOp { case equal case contains case beginsWith case endsWith case greaterThan case greaterThanEqual case lessThan case lessThanEqual } /// An individual query field. public struct FMPQueryField { /// The name of the field. public let name: String /// The value for the field. public let value: Any /// The search operator. public let op: FMPFieldOp /// Initialize with a name, value and operator. public init(name: String, value: Any, op: FMPFieldOp = .beginsWith) }
When performing an .edit
action, FMPQueryFields can be added to a FMPQuery as an array through the FMPQuery.queryFields(_ queryFields: [FMPQueryField])
function. Any field level operators are ignored in an .edit
.
When performing a .find
query fields are grouped by logical operator. Logical operators indicate how the fields should be treated together in a query. The possible logical operators are: and, or, not. Their meanings, when performing a search and selecting records for the result set, are:
- and: All query fields in the group must match for the record to be selected.
- or: Any field in the group can match and the record will be selected.
- not: If all query fields in the group match then the record will be omitted from the result set.
Multiple query field groups can be added to a FMPQuery. Each group will be considered in-order when FileMaker performs the search.
Logical operators and query field groups are represented by FMPLogicalOp & FMPQueryFieldGroup, respectively.
/// A logical operator used with query field groups. public enum FMPLogicalOp { case and, or, not } /// A group of query fields. public struct FMPQueryFieldGroup { /// The logical operator for the field group. public let op: FMPLogicalOp /// The list of fiedls in the group. public let fields: [FMPQueryField] /// Initialize with an operator and field list. /// The default logical operator is FMPLogicalOp.and. public init(fields: [FMPQueryField], op: FMPLogicalOp = .and) }
Query field groups are added through the FMPQuery.queryFields(_ queryFields: [FMPQueryFieldGroup])
function.
Examples
The following code snippets illustrate the basic activities that one would perform against FileMaker databases.
List Available Databases
This snippet connects to the server and has it list all of the hosted databases.
let fms = FileMakerServer(host: testHost, port: testPort, userName: testUserName, password: testPassword) fms.databaseNames { result in do { // Get the list of names let names = try result() for name in names { print("Got a database name \(name)") } } catch FMPError.serverError(let code, let msg) { print("Got a server error \(code) \(msg)") } catch let e { print("Got an unexpected error \(e)") } }
List Available Layouts
List all of the layouts in a particular database.
let fms = FileMakerServer(host: testHost, port: testPort, userName: testUserName, password: testPassword) fms.layoutNames(database: "FMServer_Sample") { result in guard let names = try? result() else { return // got an error } for name in names { print("Got a layout name \(name)") } }
List Field On Layout
List all of the field names on a particular layout.
let fms = FileMakerServer(host: testHost, port: testPort, userName: testUserName, password: testPassword) fms.layoutInfo(database: "FMServer_Sample", layout: "Task Details") { result in guard let layoutInfo = try? result() else { return // error } let fieldsByName = layoutInfo.fieldsByName for (name, value) in fieldsByName { print("Field \(name) = \(value)") } }
Find All Records
Perform a findAll and print all field names and values.
let query = FMPQuery(database: "FMServer_Sample", layout: "Task Details", action: .findAll) let fms = FileMakerServer(host: testHost, port: testPort, userName: testUserName, password: testPassword) fms.query(query) { result in guard let resultSet = try? result() else { return // error } let fields = resultSet.layoutInfo.fields let records = resultSet.records let recordCount = records.count for i in 0..<recordCount { let rec = records[i] for field in fields { switch field { case .fieldDefinition(let def): let fieldName = def.name if let fnd = rec.elements[fieldName], case .field(_, let fieldValue) = fnd { print("Normal field: \(fieldName) = \(fieldValue)") } case .relatedSetDefinition(let name, _): guard let fnd = rec.elements[name], case .relatedSet(_, let relatedRecs) = fnd else { continue } print("Relation: \(name)") for relatedRec in relatedRecs { for relatedRow in relatedRec.elements.values { if case .field(let fieldName, let fieldValue) = relatedRow { print("\tRelated field: \(fieldName) = \(fieldValue)") } } } } } } }
Find All Records With Skip & Max
To add skip and max, the query above would be amended as follows:
// Skip two records and return a max of two records. let query = FMPQuery(database: "FMServer_Sample", layout: "Task Details", action: .findAll) .skipRecords(2).maxRecords(2) ...
Find Records Where "Status" Is "In Progress"
Find all records where the field "Status" has the value of "In Progress".
let qfields = [FMPQueryFieldGroup(fields: [FMPQueryField(name: "Status", value: "In Progress")])] let query = FMPQuery(database: "FMServer_Sample", layout: "Task Details", action: .find) .queryFields(qfields) let fms = FileMakerServer(host: testHost, port: testPort, userName: testUserName, password: testPassword) fms.query(query) { result in guard let resultSet = try? result() else { return // error } let fields = resultSet.layoutInfo.fields let records = resultSet.records let recordCount = records.count for i in 0..<recordCount { let rec = records[i] for field in fields { switch field { case .fieldDefinition(let def): let fieldName = def.name if let fnd = rec.elements[fieldName], case .field(_, let fieldValue) = fnd { print("Normal field: \(fieldName) = \(fieldValue)") if name == "Status", case .text(let tstStr) = fieldValue { print("Status == \(tstStr)") } } case .relatedSetDefinition(let name, _): guard let fnd = rec.elements[name], case .relatedSet(_, let relatedRecs) = fnd else { continue } print("Relation: \(name)") for relatedRec in relatedRecs { for relatedRow in relatedRec.elements.values { if case .field(let fieldName, let fieldValue) = relatedRow { print("\tRelated field: \(fieldName) = \(fieldValue)") } } } } } } }