SeedCode Logo


fmxj.js
a JavaScript approach to FileMaker Custom Web Publishing™

gh LogoDownload on GitHub

postQueryFMS(query, callBackOnReady[, callBackOnDownload, phpRelay, resultClass, max, nestPortals])

Performs an Ajax call to FileMaker Server. Results are converted to an Array of Objects(JSON) and passed to the callBackOnReady handler function. Query arguments can be created from JavaScript Objects using the Query Functions. All fields on the target layout will be returned as object properties (unless a resultClass argument is passed). Properties for -recid and -modid will be added automatically to each object in the array. Portals will be returned as a nested array of objects with the related table occuremce name as the property name, prefixed with a hyphen. You can also define your own object (class) and map the FileMaker fields to your object's properties. Then pass this object as the resultClass argument(see example 5).

query
Type: String
A String of the query to be POSTed to FileMaker Server. These Strings will typically be created by the fmxj URL finctions.
callBackOnReady
Type: Function(js, utc)
A Function for handling the returned Array of Objects when ready. Supported callback arguments are:
callBackOnDownload
Type: Function(n)
(Optional) A Function for handling the on progress feedback for the POST. e.loaded is the only feedback property available from FileMaker Server and will be passed to the function as the argument n if specified.
phpRelay
Type: Object
(Optional) An object of name value pairs specifying the location of the FileMaker Server and the php relay file to use (fmxjRelay.php). Supported properties are:

You can use fmxj without any PHP providing all the JavaScript is hosted on the FileMaker Server. In this case, the JavaScript will do the httpXMLRequest POST directly to FileMaker Server's XML API. If the Guest account is not enabled then you will be prompted for FileMaker authentication from the browser. This is simple Basic Authentication, so may not be suitable for your deployment. If you're using this deployment, you can simply not pass the optional phpRelay argument or pass null, to it.

You will need to use the php Relay if your web server and Filemaker server are located remotely from each other. FileMaker server does not allow cross domain httpXMLRequests directly to the XML API, and changing this involves modifying the Web Server settings. This is actually pretty easy in Windows/IIS, but not so in Mac/FileMaker Server/Apache. In either case, dropping a single PHP relay file into the FileMaker Server's root directory is much easier.

fmxj comes with a simple PHP file (fmxjRelay.PHP) that you can use for this. You'll then do your POST to the PHP file which will then do the identical POST locally to the FileMaker server using cURL and then relay the XML results back. When doing this you'll need to have the fmxjRelay.php file in your FileMaker WPE's root directory. You'll then need to define an object in JavaScript and pass it as the phpRelay argument.

User name and password can be passed as part of the object. They are sent via POST, so can potentially be secured if both the web server and Filemaker Server are using SSL, otherwise passing the credentials like this is equivalent to Basic Authentication. You also have the option of hardcoding the FileMaker credentials in the PHP file so they're not passed via JavaScript at all.

resultClass
Type: Object
(Optional) Rather then letting the specified FileMaker layout object define your JavaScript Object, you can define an object/class and map the FileMaker values to it. This can be helpful for adapting your data to a library looking for a specific "class" of object. FileMaker values can be referenced using this syntax, where the function f will retrieve the specified field value at object creation. The method must be named getValue:

{ 
	"id" : {
		"idFieldName":"id", // filemaker field name is "id" 
		"getValue" : function(f){ // method name must be getValue.
			var field = this["idFieldName"]; //retrieve our field name
			return f(field); //retrieve the filemaker field value
		}, // end method
	}, // end property
}
In addition to straight field mapping, you can also define the getValue() method to transform/combine the source data.

{
	"allDay" : {
		"timeStartFieldName" : "TimeStart", //field for start time, 
		"getValue" : function(f){
			var field = this["timeStartFieldName"];
			if(f(field).length){  //we have a start time so this is false
				return false;
			}
			else{  //if empty, event is all day
				return true;
			};
		}, // end getValue method
	}, // end property
}
See example 5 below for more details on this argument usage.
max
Type: Number
(Optional) Passing a number to this argument will limit the number if results returned per "page." If not all query results are returned in a page then the function will POST for the next page recursively until all results are returned. 250 to 500 records seems to be ideal for FMS. See example 2.
nestPortals
Type: Boolean
(Optional) The default for this behavior is false. If set to false, then fields will not be nested. If there is a portal on the target layout and the argument is set to false, then just the first portal row will be returned (not nested). If set to true, the related fields (in portals or not) will be returned nested arrays. See example 4 below.
These examples are run from seedcode.com to a remote FileMaker Server (hosted at FoxtailTech) using the php relay.

example 1
Create a HTTP request to the hosted filemaker file "Events". Target layout in the specified file is "Events". Query is created by the findRecordsURL() function and passed as the query argument. The required handler for onreadystateexchange is defined as well as the optional onprogress handler.

var requests =	[ //create requests for query
	{ "DateStart" : "<=2/28/2014" , "DateEnd" : ">=2/1/2014" },
	{ "DateStart" : "2/1/2014...2/28/2014" }
				];
var query = fmxj.findRecordsURL("Events", "Events", requests) ; // create query

function onReadyFuntion(js){ //define handler for onready
	document.getElementById("example1") = JSON.stringify(js, null, 4);
} ;
function onProgressFuntion(n){ //define handler for onprogress
	document.getElementById("example1") += n + " bytes downloaded\n";
} ;
fmxj.postQueryFMS(query, onReadyFunction, onProgressFunction); //make call


example 2
Looping Ajax Calls. Specify a max argument to limit the results per page. The function will recursively return the next page of results, and call the onReadyfunction until all records found in the query are returned.

var sourceLoop = [];
var requests =	[ //create requests for query
	{ "DateStart" : "<=2/28/2014" , "DateEnd" : ">=2/1/2014" },
	{ "DateStart" : "2/1/2014...2/28/2014" }
				];
var sortObject = { 
	"field1" : "DateStart" ,
	"order1" : "ascend" ,
				 } ;
var query = fmxj.findRecordsURL("Events", "Events", requests) ; // create query
function onReadyFuntion(js){ //define handler for onready
	//callBack will return pages recursively, so concat to our array.
	sourceLoop = sourceLoop.concat(js);
	document.getElementById("example1") = JSON.stringify(sourceLoop, null, 4);
};
// for 375 results per page (max) we don't need an onProgress handler, so we'll set to null.
fmxj.postQueryFMS(query, onReadyFunction, null, relay, null, 375); //make call


example 3
Errors returned as a simple one object array with the Filemaker error code and description.

var query = fmxj.findRecordsURL("Events", "ShmEvents", requests) ;
fmxj.postQueryFMS(query, onReadyFunction) ;

example 4
Portals on the target layouts can be converted to nested arrays. By specifying the nestPortals argument as true, the TO name of the portal's source will be used as the property name, and will be prefixed by a hyphen to make sure it doesn't collide with a field name in the parent table. This puts a higher load on the server, so use sparingly or consider an alternitive structure using the nestObjects() function. The argument is false by default, so you must pass true for the nesting.

var query = fmxj.findRecordsURL("Contacts", "Contacts");
fmxj.postQueryFMS(q, writeResults, writeDownload, relay, null, 1000, true);



example 5
Specify an object "class" for the objects rather than using the FileMaker layout and field names to define them. This can be used for straight field mapping, but also for transformation. The property name of the object will be the new object property name. The property value is an object specifying the FileMaker fields to be used and a method getValue to specify how the values should be returned. Please use the below syntax for the getValue method. This example uses moment.js. FileMaker Times are "floating" so we need to specify a time zone to solidify the events in the continuum.

var fcObject = 	{
	"id" : {
			"idField":"id",
			"getValue" : function(f){
				var field = this["idField"];
				return f(field);
			},
		},
	"title" : {
			"titleField":"Summary",
			"getValue" : function(f){
				var field = this["titleField"];
				return f(field);
			},
		},
	"allDay" : {
			"timeStartField" : "TimeStart",
			"getValue" : function(f){
				var field = this["timeStartField"];
				if(f(field).length){//we have a start time so this is false
					return false;
				}
				else{
					return true;
				}
			},
		},
	"start" : {
		"timeStartField" : "TimeStart",
		"dateStartField" : "DateStart",
		"yearFormat" : "MM-DD-YYYY",
		"timeFormat" : "HH:mm",
		"timezone" : "America/Los_Angeles",
		"getValue" : function(f){
			var time = this["timeStartField"];
			var date = this["dateStartField"];
			var zone = this["timezone"];
			var yearFormat = this["yearFormat"];
			var timeFormat = this["timeFormat"];
			var date = moment( f(date) + " " + f(time) , yearFormat + " " + timeFormat );
			return date.tz(zone).format();
		},
	},
	"end" : {
		"timeEndField" : "TimeEnd",
		"dateEndField" : "DateEnd",
		"dateStartField" : "DateStart", //if no end date is specified we'll need to use the start date.
		"yearFormat" : "MM-DD-YYYY",
		"timeFormat" : "HH:mm",
		"timezone" : "America/Los_Angeles",
		"getValue" : function(f){
			var time = this["timeEndField"];
			var date = this["dateEndField"];
			var sdate = this["dateStartField"];
			var zone = this["timezone"];
			var yearFormat = this["yearFormat"];
			var timeFormat = this["timeFormat"];
			//use start date if no end date
			if(!f(date)){ var d = f(sdate) } else { var d = f(date)};
			if(f(time).length){
				d = moment( d + " " + f(time) , yearFormat + " " + timeFormat );
			}
			else
			{
				d = moment( d , yearFormat + " " + timeFormat ).add( 1, "days");
			};
			return d.tz(zone).format();
		},
	},
	"description" : {
			"descriptionField":"Description",
			"getValue" : function(f){
				var field = this["descriptionField"];
				return f(field);
			},
		},
	"resource" : {
			"resourceField":"Resource",
			"getValue" : function(f){
				var field = this["resourceField"];
				return f(field);
			},
		},
	"status" : {
			"statusField":"Status",
			"getValue" : function(f){
				var field = this["statusField"];
				return f(field);
			},
		},
	"contactId" : {
			"contactIdFieldName":"id_contact",
			"getValue" : function(f){
				var field = this["contactIdFieldName"];
				return f(field);
			},
		},
	"projectId" : {
			"projectIdField":"id_project",
			"getValue" : function(f){
				var field = this["projectIdField"];
				return f(field);
			},
		},
	"fmRecordId" : {
			"fmRecordIdField":"-recid",
			"getValue" : function(f){
				var field = this["fmRecordIdField"];
				return f(field);
			},
		},
	"fmModId" : {
			"fmModIdField":"-modid",
			"getValue" : function(f){
				var field = this["fmModIdField"];
				return f(field);
			},
		},
	};
//build query and make call passing are fcObject as the resultClass argument
var query = fmxj.findRecordsURL("Events", "Events", requests);
fmxj.postQueryFMS(query, writeResults, writeDownload, relay, fcObject);