├── LICENSE
├── README.md
├── css
└── fmxj.css
├── databases
├── Contacts.fmp12
└── Events.fmp12
├── examples
├── deleteRecord.html
├── editRecord.html
├── fileNames.html
├── filterObjects.html
├── findRecords.html
├── layoutFields.html
├── layoutNames.html
├── nestObjects.html
├── postQuery.html
└── sortObjects.html
├── fmxj.js
├── fmxjDemo.js
├── fmxjRelay.php
├── gh.png
├── index.html
├── libraries
├── moment-timezone.js
├── moment.js
├── prism.css
└── prism.js
└── logowhite.png
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 SeedCode
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # fmxj.js
2 | ## A Javascript based approach to FileMaker Custom Web Publishing
3 |
4 | ### With fmxj.js and simple JavaScript Objects you can
5 |
6 | * Build complex queries and POST them to FileMaker Server
7 | * Return FileMaker parent and child records as JavaScript Objects/JSON
8 | * Create, edit and delete FileMaker records
9 | * Filter and sort Javascript objects locally with complex criteria.
10 |
11 | Working examples and basic function descriptions are available at the fmxj example page.
12 |
13 | fmxj.js does not have any dependencies, but does use prism.js for the demo page code blocks and moment.js for the postQueryFMS() demo page resultClass example.
14 |
15 | fmxj.js is designed to do the data interchange work with FileMaker Server in JavaScript. Query strings are created from JavaScript Objects and then sent as a POST to FileMaker's XML Web Publishing Engine. An XML FMPXMLRESULT is returned and converted into JavaScript Objects/JSON by fmxj. The core idea is to do as little as possible on the FileMaker server and shift what we can to the client and JavaScript processes. Supporting FileMaker's Web Publishing can be challenging so removing as many variabes as possible is one of our goals with this project.
16 |
17 | POSTS can be done directly to the FileMaker Server's XML WPE or a simple PHP relay can be used to get around cross-domain issues and provide more secure authentication options. With the goal of doing as little as possible on the Server, we were a little disappointed to need to rely on PHP at all, but there are some limitations to interacting with the FMS XML API directly, and a PHP relay is the simplest way to get around them. Specifically it's much easier to add access headers to a PHP page than it is to Apache! See more on the PHP relay below in the **postQueryFMS()** function.
18 |
19 | ## Functions for working with FileMaker Server
20 | The **postQueryFMS()** is the primary function used for POSTing queries to FileMaker Server via httpXMLRequest and then converting the FMPXMLRESULT xml results into JavaScript objects for callback. Queries can be created easily from JavaScript objects using the fmxj URL functions below.
21 |
22 | ***
23 | **postQueryFMS(query, callBackOnReady[, callBackOnDownload, phpRelay, classResult, max, nestPortals])**
24 |
25 | * **query:** string: The query, built by one of the fmxj URL functions
26 | * **callBackOnReady:** function: The handler function for the returned array of objects.
27 | * **callBackOnDownload:** (optional) function: The handler function for the POST *e.loaded* progress reports.
28 | * **phpRelay:** (optional) object: specifies the server address and name of the php relay file being used.
29 | * **classResult:** (optional) object: defines "classes" the result objects rather then letting the FileMaker layout do this.
30 | * **max:** (optional) number: limts the number of 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. This argument will override any max argument specified in the query object, but not the skip.
31 | * **nestPortals:** (optional) boolean: Determines whether realted fields on the target layout will be treated as nested arrays. If set to *true*, field values will become nested arrays regardless of number of fields, number of portal rows or even if no portal is present on the layout. If set to *false* then the field values will be set to the top level of the object with their full related field name. If set to false and there is a portal on the layout then just the first portal row will be returned, not nested. Default is false.
32 |
33 | An optional handler function can be passed as well to report the download progress from FileMaker Server. Note that FileMaker server does not pass the *e.total* property in it's progres reporting, only the bytes downloded *e.loaded*.
34 |
35 | **Example of formatted results (stringified JSON) returned by the postQueryFMS function for a find query:**
36 |
37 | ```json
38 | [
39 | {
40 | "-recid": "6198",
41 | "-modid": "116",
42 | "DateEnd": "02/09/2014",
43 | "DateStart": "02/09/2014",
44 | "Description": "",
45 | "id": "7A2E442C-3782-497F-A539-4495F1B28806",
46 | "id_contact": "ED3C1292-EBC2-4027-82D2-33ADFB590A1D",
47 | "id_Phase": "",
48 | "id_Project": "P00031",
49 | "Resource": "Example A",
50 | "Status": "Open",
51 | "Summary": "Begin Production",
52 | "TimeEnd": "",
53 | "TimeStart": "",
54 | "z_LinkedWithinProject": "1",
55 | "z_MilestoneSort": "",
56 | "z_Notified": "",
57 | "z_Repeating_id": ""
58 | },
59 | {
60 | "-recid": "6199",
61 | "-modid": "5",
62 | "DateEnd": "02/10/2014",
63 | "DateStart": "02/10/2014",
64 | "Description": "",
65 | "id": "D546B0FF-E87C-41CD-8B1C-A3B291E7FCCD",
66 | "id_contact": "2C2CD8FB-EFD9-44E9-91B0-884C01D887EB",
67 | "id_Phase": "",
68 | "id_Project": "P00031",
69 | "Resource": "Example B",
70 | "Status": "Open",
71 | "Summary": "Begin Production",
72 | "TimeEnd": "",
73 | "TimeStart": "",
74 | "z_LinkedWithinProject": "1",
75 | "z_MilestoneSort": "",
76 | "z_Notified": "",
77 | "z_Repeating_id": ""
78 | },
79 | {
80 | "-recid": "6201",
81 | "-modid": "4",
82 | "DateEnd": "02/12/2014",
83 | "DateStart": "02/12/2014",
84 | "Description": "",
85 | "id": "9F86C241-8A64-4B82-9B58-CEB5131FA125",
86 | "id_contact": "D4C1F32F-C2D5-4584-947C-4F409502C157",
87 | "id_Phase": "",
88 | "id_Project": "P00031",
89 | "Resource": "Example D",
90 | "Status": "Open",
91 | "Summary": "Begin Production",
92 | "TimeEnd": "",
93 | "TimeStart": "",
94 | "z_LinkedWithinProject": "1",
95 | "z_MilestoneSort": "",
96 | "z_Notified": "",
97 | "z_Repeating_id": ""
98 | }
99 | ]
100 | ```
101 | **Example of formatted results (stringified JSON) returned by the postQueryFMS function for a find query with a Portal on the target layout.** **nestPortals** must be set to *true* for this nesting to take place, otherwise just the first portal row will be returned at the top level. *(We recommend using this sparingly as it can put a significantly higher load on the server, and we'll be exploring some client side alternitives to this.)*
102 |
103 | ```json
104 | [
105 | {
106 | "-recid": "1",
107 | "-modid": "0",
108 | "id": "338794336203723354493363901762748606262",
109 | "NameFirst": "Diane",
110 | "NameLast": "Ort",
111 | "Status": "Customer",
112 | "-ContactInfo": [
113 | {
114 | "Type": "Email",
115 | "Value": "diane@ort.com"
116 | },
117 | {
118 | "Type": "Phone",
119 | "Value": "(415) 393-6166"
120 | },
121 | {
122 | "Type": "URL",
123 | "Value": "http://www.dianeort.com"
124 | }
125 | ]
126 | },
127 | {
128 | "-recid": "2",
129 | "-modid": "0",
130 | "id": "262473748365757206478007125406773627812",
131 | "NameFirst": "Zane",
132 | "NameLast": "Opunui",
133 | "Status": "Customer",
134 | "-ContactInfo": [
135 | {
136 | "Type": "Email",
137 | "Value": "zane@opunui.com"
138 | },
139 | {
140 | "Type": "Phone",
141 | "Value": "(434) 296-3666"
142 | },
143 | {
144 | "Type": "URL",
145 | "Value": "http://www.zaneopunui.com"
146 | }
147 | ]
148 | },
149 | {
150 | "-recid": "3",
151 | "-modid": "1",
152 | "id": "24517341939382263356133708272977092954",
153 | "NameFirst": "Lea",
154 | "NameLast": "Knighton",
155 | "Status": "Customer",
156 | "-ContactInfo": [
157 | {
158 | "Type": "Email",
159 | "Value": "lea@knighton.com"
160 | },
161 | {
162 | "Type": "Phone",
163 | "Value": "(702) 878-2130"
164 | },
165 | {
166 | "Type": "URL",
167 | "Value": "http://www.leaknighton.com"
168 | }
169 | ]
170 | }
171 | ]
172 | ```
173 | **Deployment and the phpRelay**
174 |
175 | 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.
176 |
177 | 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.
178 |
179 | 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.
180 |
181 | **Supported Object Properties Are:**
182 |
183 | * **"php":** the php file name (required)
184 | * **"server":** the filemaker server address (optional, you may use the php file with the JS running locally)
185 | * **"protocol":** http / https (optional)
186 | * **"port"":** 80/443 (optional)
187 | * **"user":** FileMaker Account Name (optional)
188 | * **"pass":** FileMaker Account Password (optional)
189 |
190 | 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.
191 |
192 | **Relay Examples**
193 |
194 | ```javascript
195 | //create two objects in a JSON array, each one is a FileMaker Find Request
196 | var requests = [{"Resources":"Example A"},{"Resources":"Example B"}];
197 |
198 | //build query from our array
199 | var query = fmxj.findRecordsURL("Events","Events" , requests);
200 |
201 | //specify FileMaker Server we're posting to
202 | var relay = {"php":"fmxjRelay.php","server":"seedcode.com"};
203 |
204 | //now do the POST (without any on Download handler);
205 | fmxj.postQueryFMS(query, onReadyFunction, null, relay);
206 | ```
207 |
208 | POSTING FileMaker credentials to a secure server would use an object like this:
209 |
210 | ```javascript
211 | //specify FileMaker https Server we're posting to with credentials
212 | var relay = {"php":"fmxjRelay.php","server":"seedcode.com","protocol":"https","port":"443","user":varUser,"pass":varPass};
213 | ```
214 |
215 | **Remember!** Both the Web Server and the FileMaker Server need to be runnning SSL for this transaction to be secure. There's a good article on this [here](http://www.troyhunt.com/2013/05/your-login-form-posts-to-https-but-you.html).
216 |
217 | **classResult Example**
218 |
219 | Specify an object "class" for the results using the below syntax and pass it as the resultClass argument. This example uses moment.js. FileMaker Times are "floating" so we need to specify a time zone to solidify the events in the continuum.
220 |
221 | ```javascript
222 | var requests = [
223 | { "DateStart" : "<=2/28/2014" , "DateEnd" : ">=2/1/2014" } ,
224 | { "DateStart" : "2/1/2014...2/28/2014" }
225 | ] ;
226 | var query = fmxj.findRecordsURL("Events", "Events", requests);
227 | var query = fmxj.findRecordsURL("Events", "Events", requests);
228 | //define object class for the results and pass that as the class argument.
229 | var fcObject = {
230 | "id" : {
231 | "idField":"id",
232 | "getValue" : function(f){
233 | var field = this["idField"];
234 | return f(field);
235 | },
236 | },
237 | "title" : {
238 | "titleField":"Summary",
239 | "getValue" : function(f){
240 | var field = this["titleField"];
241 | return f(field);
242 | },
243 | },
244 | "allDay" : {
245 | "timeStartField" : "TimeStart",
246 | "getValue" : function(f){
247 | var field = this["timeStartField"];
248 | if(f(field).length){//we have a start time so this is false
249 | return false;
250 | }
251 | else{
252 | return true;
253 | }
254 | },
255 | },
256 | "start" : {
257 | "timeStartField" : "TimeStart",
258 | "dateStartField" : "DateStart",
259 | "yearFormat" : "MM-DD-YYYY",
260 | "timeFormat" : "HH:mm",
261 | "timezone" : "America/Los_Angeles",
262 | "getValue" : function(f){
263 | var time = this["timeStartField"];
264 | var date = this["dateStartField"];
265 | var zone = this["timezone"];
266 | var yearFormat = this["yearFormat"];
267 | var timeFormat = this["timeFormat"];
268 | var date = moment( f(date) + " " + f(time) , yearFormat + " " + timeFormat );
269 | return date.tz(zone).format();
270 | },
271 | },
272 | "end" : {
273 | "timeEndField" : "TimeEnd",
274 | "dateEndField" : "DateEnd",
275 | "dateStartField" : "DateStart", //if no end date is specified we'll need to use the start date.
276 | "yearFormat" : "MM-DD-YYYY",
277 | "timeFormat" : "HH:mm",
278 | "timezone" : "America/Los_Angeles",
279 | "getValue" : function(f){
280 | var time = this["timeEndField"];
281 | var date = this["dateEndField"];
282 | var sdate = this["dateStartField"];
283 | var zone = this["timezone"];
284 | var yearFormat = this["yearFormat"];
285 | var timeFormat = this["timeFormat"];
286 | //use start date if no end date
287 | if(!f(date)){ var d = f(sdate) } else { var d = f(date)};
288 | if(f(time).length){
289 | d = moment( d + " " + f(time) , yearFormat + " " + timeFormat );
290 | }
291 | else
292 | {
293 | d = moment( d , yearFormat + " " + timeFormat ).add( 1, "days");
294 | };
295 | return d.tz(zone).format();
296 | },
297 | },
298 | "description" : {
299 | "descriptionField":"Description",
300 | "getValue" : function(f){
301 | var field = this["descriptionField"];
302 | return f(field);
303 | },
304 | },
305 | "resource" : {
306 | "resourceField":"Resource",
307 | "getValue" : function(f){
308 | var field = this["resourceField"];
309 | return f(field);
310 | },
311 | },
312 | "status" : {
313 | "statusField":"Status",
314 | "getValue" : function(f){
315 | var field = this["statusField"];
316 | return f(field);
317 | },
318 | },
319 | "contactId" : {
320 | "contactIdFieldName":"id_contact",
321 | "getValue" : function(f){
322 | var field = this["contactIdFieldName"];
323 | return f(field);
324 | },
325 | },
326 | "projectId" : {
327 | "projectIdField":"id_project",
328 | "getValue" : function(f){
329 | var field = this["projectIdField"];
330 | return f(field);
331 | },
332 | },
333 | "fmRecordId" : {
334 | "fmRecordIdField":"-recid",
335 | "getValue" : function(f){
336 | var field = this["fmRecordIdField"];
337 | return f(field);
338 | },
339 | },
340 | "fmModId" : {
341 | "fmModIdField":"-modid",
342 | "getValue" : function(f){
343 | var field = this["fmModIdField"];
344 | return f(field);
345 | },
346 | },
347 | };
348 | //make call with our custom object definition
349 | fmxj.postQueryFMS(query, writeResults, writeDownload, relay, fcObject);
350 | ```
351 |
352 | results in objects like this:
353 |
354 | ```json
355 | {
356 | "id": "7A2E442C-3782-497F-A539-4495F1B28806",
357 | "title": "Begin Production",
358 | "allDay": true,
359 | "start": "2014-02-08T23:00:00-08:00",
360 | "end": "2014-02-09T23:00:00-08:00",
361 | "description": "",
362 | "resource": "Example A",
363 | "status": "Open",
364 | "contactId": "ED3C1292-EBC2-4027-82D2-33ADFB590A1D",
365 | "projectId": "P00031",
366 | "fmRecordId": "6198",
367 | "fmModId": "178"
368 | }
369 | ```
370 |
371 | ***
372 | ## Data Functions
373 |
374 | These three functions are used to build the specific query type strings for the **postQueryFMS** function to POST. The idea being that you can use existing objects or simple JSON to create complex query strings.
375 |
376 | ***
377 | **findRecordsURL(fileName, layoutName, requests[, sort, max, skip])**
378 |
379 | * **fileName:** string: The target FileMaker file
380 | * **layoutName:** string: The target FileMaker layout in the above refernced file
381 | * **requests:** array of objects: Each object in the array represents a FileMaker find request
382 | * **sort:** (optional) object: Specifies a sort order for the query, options are ascend, descend or value list name
383 | * **max:** (optional) number: The maximum number of records/objects to return
384 | * **skip:** (optional) object: The number of rows to skip (like offset) before returning records/objects
385 |
386 | **Example**
387 |
388 | ```javascript
389 | //create two objects in a JSON array, each one is a FileMaker Find Request.
390 | //will find all records with Resources = Example A OR Resources = Example B.
391 | var requests = [{"Resources":"Example A"},{"Resources":"Example B"}];
392 |
393 | // we want to sort these by StartDate descending and Resource ascending, so create an Object to define that.
394 | var sort = {"field1":"DateStart","sort1":"descend","field2":"Resource","sort2":"ascend"};
395 |
396 | //build query from our array, our file and layout name are "Events"
397 | var query = fmxj.findRecordsURL("Events", "Events", requests, sort);
398 | ```
399 |
400 | **...returns:**
401 |
402 | -db=Events&-lay=Events&-query=(q1);(q2)&-q1=Resources&-q1.value=Example A&-q2=Resources&-q2.value=Example B&-sortfield.1=DateStart&-sortorder.1=descend&-sortfield.2=Resource&-sortorder.2=ascend&-findquery
403 |
404 | ...which can now be passed to *postQueryFMS()*.
405 |
406 | To specify a request as an *Omit* request, simply specify an -omit property in the object as 1, e.g.
407 |
408 | ```javascript
409 | var requests = [{"Resources":"Example A","-omit":"1"}];
410 | ```
411 |
412 | **...returns a query string for omiting all the records where the Resource is equal to Example A:**
413 |
414 | -db=Events&-lay=Events&-query=!(q1)&-q1=Resources&-q1.value=Example A&-findquery
415 |
416 |
417 | ***
418 | **editRecordURL(fileName, layoutName, editObj)**
419 |
420 | * **fileName:** string: The target FileMaker file
421 | * **layoutName:** string: The target FileMaker layout in the above refernced file
422 | * **editObj:** object: An object where the properties represent the fields to be edited.
423 |
424 | This function will create a -edit query for a FileMaker record if the -recid property is specified. This represents the FileMaker Record ID of the record to edit. Optionally a -modid property can be specified. See the FileMaker CWP XML guide for more info in using the -modid.
425 |
426 | If the -recid property is not specified, then this function will create a -new query for generating a new record.
427 |
428 | **-edit Example**
429 |
430 | ```javascript
431 | //Edit the Resource value of the record with a -recid of 6198.
432 | //Edit the value to Example A
433 | var edit = {"-recid":"6198","Resources":"Example A"};
434 |
435 | //build query from our object, our file and layout name are "Events"
436 | var query = fmxj.editRecordURL("Events", "Events", edit);
437 | ```
438 |
439 | **...returns:**
440 |
441 | -db=Events&-lay=Events&-recid=6198&Resources=Example A&-edit
442 |
443 | **-new Example**
444 |
445 | ```javascript
446 | //Create a new record with Resources set to Example A and StartDate = to 1/11/2015
447 | var newRecord = {"Resources":"Example A","StartDate":"1/11/2015"};
448 |
449 | //build query from our object, our file and layout name are "Events"
450 | var query = fmxj.editRecordURL("Events", "Events", newRecord);
451 | ```
452 |
453 | **...returns:**
454 |
455 | -db=Events&-lay=Events&Resources=Example A&StartDate=1/11/2015&-new
456 |
457 | ...these queries can now be passed to *postQueryFMS()*.
458 |
459 | ***
460 | **deleteRecordURL(fileName, layoutName, recid)**
461 |
462 | * **fileName:** string: The target FileMaker file
463 | * **layoutName:** string: The target FileMaker layout in the above refernced file
464 | * **recid:** number: The -recid / Record ID of the record to delete
465 |
466 | This function will create a -delete query for a FileMaker record with the specified -recid property.
467 |
468 | **Example**
469 |
470 | ```javascript
471 | //Delete the record with a -recid of 6198.
472 | //build query from our recid, our file and layout name are "Events"
473 | var query = fmxj.deleteRecordURL("Events" , "Events" , 6198);
474 | ```
475 |
476 | **...returns:**
477 |
478 | -db=Events&-lay=Events&-recid=6198&-delete
479 |
480 | ...which can now be passed to *postQueryFMS()*.
481 |
482 | ***
483 | ## Design Functions
484 |
485 | These three functions are used to build the specific query type strings for the **postQueryFMS** function to POST. These three are for getting facts about the files on the server. The layouts in a file, and the fields on a layout.
486 |
487 | ***
488 | **fileNamesURL()**
489 |
490 | * **fileName:** string: The target FileMaker file
491 | * **layoutName:** string: The target FileMaker layout in the above refernced file
492 |
493 | This function will create a -dbnames query that we'll use to get a list of the available files on the server. Files will need to have at least one account with XML extended privileges to show up here. This query is slower than the rest.
494 |
495 | **Example**
496 |
497 | ```javascript
498 | //Get the list of file names on the specified or host server
499 | var query = fmxj.fileNamesURL();
500 | ```
501 |
502 | **...returns:**
503 |
504 | -dbnames
505 |
506 | ...which can now be passed to *postQueryFMS()*.
507 |
508 | ***
509 | **layoutNamesURL(fileName)**
510 |
511 | * **fileName:** string: The target FileMaker file
512 |
513 | This function will create a -layoutnames query that we'll use to get a list of layout names for the specified file.
514 |
515 | **Example**
516 |
517 | ```javascript
518 | //Get the list of fields and their types on the "Evemts" layout
519 | //build query from our recid, our file and layout name are "Events"
520 | var query = fmxj.LayoutNamesURL("Events");
521 | ```
522 |
523 | **...returns:**
524 |
525 | -db=Events&-layoutnames
526 |
527 | ...which can now be passed to *postQueryFMS()*.
528 |
529 | ***
530 | **layoutFieldsURL(fileName, layoutName)**
531 |
532 | * **fileName:** string: The target FileMaker file
533 | * **layoutName:** string: The target FileMaker layout in the above refernced file
534 |
535 | This function will create a -findany query that we'll use to get a one record result, but just return the layout fields and their data types. We use -findany, because FMPXMLLAYOUT does not give us the actual field types, but just the field control style on the layout. The postQueryFMS() recognizes the -findany and uses that as a flag to return the fields/types instead of actual data.
536 |
537 | **Example**
538 |
539 | ```javascript
540 | //Get the list of fields and their types on the "Evemts" layout
541 | //build query from our recid, our file and layout name are "Events"
542 | var query = fmxj.LayoutFieldsURL("Events" , "Events");
543 | ```
544 |
545 | **...returns:**
546 |
547 | -db=Events&-lay=Events&-findany
548 |
549 | ...which can now be passed to *postQueryFMS()*.
550 |
551 |
552 | ***
553 | ## Functions for working with JavaScript Objects
554 | Functions for handling your objects in JavaScript. One of the ideas of fmxj is to have the FileMaker server do as little work as possible. We want to get our data with small Ajax calls and any kind of necessary scripting in JavaScript.
555 |
556 | *We do have a php deployment option, but the php page is set up to do as little as possible. It takes our POST then relays it via cURL to the FileMaker Server. It then returns the raw FMPXMLRESULT for fmxj to convert to objects. We don't anticipate needing to (or wanting to) enhance this server side processing. Script running arguments were intentionally left off the findRecordsURL() function for the same reason. We weren't even sure about including the sort argument and supporting Portals as nested arrays, but they are in there now.*
557 |
558 | These functions are for that client side processing, and we anticipate (hope) that this is the part library that grows!
559 |
560 | ***
561 | **filterObjects(filters, searchTypes, source)**
562 |
563 | * **filters:** array of objects: each object represents a "FileMaker" type requests
564 | * **searchTypes:** object: specifies the search type to perform on the specified property. Supported types are:
565 | * contains
566 | * startswith
567 | * equals
568 | * **source:** array of objects: objects to be filtered
569 |
570 | This function creates a new array of objects from a source array based on filters. Each **filters** object represents a FileMaker type find request where each set of name value pairs within the object are *AND* clauses and each object represents an *OR* clause. **searchTypes** alloes you to choose from one of the pre-configured regex searches set up in the function. You can add your own types by adding them to the **generatRegEx()** function that lives inside **filteObjects.**
571 |
572 | ``` javascript
573 | //regExp logic is here: for string, use "begins with" by default.
574 | var generateRegEx = function ( v , dt ) {
575 | var re = new RegExp();
576 | if (dt.toUpperCase() === "STARTSWITH") {
577 | re = new RegExp ( "^" + v + "|\\s" + v , "mi" ) ;
578 | }
579 | else if (dt.toUpperCase() === "EQUALS") {
580 | re = new RegExp ( "^" + v + "$" ) ;
581 | }
582 | else if (dt.toUpperCase() === "CONTAINS") {
583 | re = new RegExp ( "\\" + v + "\\" , "mi" ) ;
584 | };
585 | return re;
586 | };
587 | ```
588 | **Example**
589 |
590 | ``` javascript
591 | var requests = [
592 | { "id" : "E4B04F12-E006-4928-A1E0-0E86EDF5641C" } ,
593 | { "id" : "463BBEA9-404B-4979-8CC0-6F8F60EB0154" } ,
594 | { "id" : "8CDA64C4-643D-4A64-9336-83BEF07F0CF4" } ,
595 | ] ;
596 | var types = { "id" : "equals" , "Status" : "equals" } ;
597 | fmxj.filterObjects( requests , types , source ) ;
598 | ```
599 |
600 | ***
601 | **sortObjects(sortOrder, source)**
602 |
603 | * **sortOrder:** object: specifies the properties to sort, their sort order and data types.
604 | supported values for order are ascend and descend. Supported types are:
605 | * string (default, if not specified)
606 | * number
607 | * date (use for timestamps too)
608 | * time (of day)
609 | * **source:** array of objects: objects to be sorted
610 |
611 | This function sorts the specified array of objects by 1-n properties, specifying the data type for the property.
612 |
613 | **Example**
614 |
615 | ``` javascript
616 | var sort = {
617 | "field1" : "Resource" ,
618 | "order1" : "ascend" ,
619 | "type1" : "string" ,
620 | "field2" : "DateStart" ,
621 | "order2" : "descend" ,
622 | "type2" : "date"
623 | } ;
624 | fmxj.sortObjects(sort, dataTypes, source) ;
625 | ```
626 |
627 | ***
628 |
629 | **nestObjects(parentArray, childArray, childName, predicates)**
630 |
631 | * **parentArray:** array of object: An Array of Objects that will be the parent objects and have the child array nested in them.
632 | * **childArray:** array of objects: An Array of Objects to be nested into the above parent objects.
633 | * **childArray:** string: The name of the property of the child array in the parent object.
634 | * **predicates:** object: An object specifying the match keys to join parent and child objects. Only equijoin matches supported.
635 | * parentKey1 - parentKeyn: match key properties in the parent objects.
636 | * childKey1 - childKeyn: match key properties in the child objects.
637 |
638 | Nest one array of objects into another as a property a SQL like Join.
639 |
640 | **Example**
641 |
642 | ``` javascript
643 | fmxj.nestObjects(contacts, contactinfo, "ContactInfo", {"parentKey1": "id", "childKey1": "id_Contact"});
644 | ```
645 |
646 | ***
647 |
648 |
649 |
650 |
651 |
652 |
653 |
654 |
--------------------------------------------------------------------------------
/css/fmxj.css:
--------------------------------------------------------------------------------
1 |
2 | body {
3 | width:94%;
4 | min-width:800px;
5 | background-color: rgb(88,93,118);
6 | }
7 |
8 | div {
9 | font-family:Cabin,Arial,Verdana,sans-serif;
10 | font-weight:lighter;
11 | }
12 |
13 | footer {
14 | height:36px;
15 | }
16 |
17 |
18 | button {
19 | margin-top:12px;
20 | padding-top:18px;
21 | padding-bottom:18px;
22 | font-size:100%;
23 | color:white;
24 | border-radius:5px;
25 | outline:0px;
26 | cursor:pointer;
27 | width:100%;
28 | -webkit-box-shadow: 1px 1px 1px rgba(240,240,240,0.8);
29 | -moz-box-shadow: 1px 1px 1px rgba(240,240,240,0.8);
30 | box-shadow: 1px 1px 1px rgba(240,240,240,0.8);
31 | border-top-color: rgba(255, 255, 255, 0.898039);
32 | border-top-style: solid;
33 | border-top-width: 1px;
34 | border-right-color: rgba(255, 255, 255, 0.898039);
35 | border-right-style: solid;
36 | border-right-width: 1px;
37 | border-bottom-color: rgba(255, 255, 255, 0.898039);
38 | border-bottom-style: solid;
39 | border-bottom-width: 1px;
40 | border-left-color: rgba(255, 255, 255, 0.898039);
41 | border-left-style: solid;
42 | border-left-width: 1px;
43 | background:rgba(88,93,118,0.94);
44 | }
45 |
46 | button:hover{
47 | color:#a7d7ff;
48 | //color:rgba(240, 240, 240, .85);
49 | background:rgba(88,93,118, 0.98);
50 | }
51 |
52 | li > a {
53 | text-decoration:none;
54 | color:white;
55 | }
56 |
57 | li > a:hover {
58 | //color:rgba(240, 240, 240, .85);
59 | color:#a7d7ff;
60 | cursor:pointer;
61 | text-decoration:underline;
62 | }
63 |
64 | h3{
65 | font-weight:lighter;
66 | margin-top:0px;
67 | margin-bottom:0px;
68 | }
69 |
70 | h3 > a {
71 | color:rgb(88,93,118);
72 | text-decoration:none;
73 | }
74 |
75 | h3 > a:hover {
76 | text-decoration:underline;
77 | }
78 |
79 | button > a {
80 | text-decoration:none;
81 | color:white;
82 | }
83 |
84 | pre {
85 | overflow:auto;
86 | padding:0px 12px 12px 12px;
87 | }
88 |
89 | code {
90 | overflow-x:scroll;
91 | font-size:87%;
92 | border-right: 1px solid #999;
93 | }
94 |
95 |
96 | .title {
97 | color:white;
98 | font-size:54px;
99 | margin-left:54px;
100 | margin-bottom:14px;
101 | }
102 |
103 | .sub-title {
104 | color:white;
105 | margin-left:54px;
106 | padding:0px;
107 | font-size:24px;
108 | margin-bottom:-6px;
109 | }
110 |
111 | div.sidebar {
112 | padding:12px 1% 5px 0px;
113 | margin-top:18px;
114 | margin-left:36px;
115 | width:200px;
116 | float:left;
117 | min-height:300px;
118 | border-radius: 4px 0px 0px 4px;
119 | border-top-color: rgba(255, 255, 255, 0.298039);
120 | border-top-style: solid;
121 | border-top-width: 1px;
122 | border-bottom-color: rgba(255, 255, 255, 0.298039);
123 | border-bottom-style: solid;
124 | border-bottom-width: 1px;
125 | border-left-color: rgba(255, 255, 255, 0.298039);
126 | border-left-style: solid;
127 | border-left-width: 1px;
128 | color:white;
129 | background:rgba(167, 215, 255, 0.0980392);
130 | font-weight:lighter;
131 |
132 | }
133 |
134 | div.content {
135 | background:rgba(248, 248, 248, .9 );
136 | border-radius: 0px 4px 4px 4px;
137 | padding:20px 20px 12px 20px;
138 | margin-left:248px;
139 | margin-top:18px;
140 | min-height:800px;
141 | border-top-color: rgba(255, 255, 255, 0.298039);
142 | border-top-style: solid;
143 | border-top-width: 1px;
144 | border-right-color: rgba(255, 255, 255, 0.298039);
145 | border-right-style: solid;
146 | border-right-width: 1px;
147 | border-bottom-color: rgba(255, 255, 255, 0.298039);
148 | border-bottom-style: solid;
149 | border-bottom-width: 1px;
150 | border-left-color: rgba(255, 255, 255, 0.298039);
151 | border-left-style: solid;
152 | border-left-width: 1px;
153 | color:rgb(88,93,118);
154 | }
155 |
156 |
157 |
158 | li.largeitem {
159 | font-size:large;
160 | margin-left:2%;
161 | }
162 |
163 | li.smallitem {
164 | margin-left:-12px;
165 | font-size:90%;
166 | padding:8px 0px 8px 0px;
167 | font-weight:lighter;
168 | list-style-type: square;
169 | }
170 |
171 | li.sidebaritem {
172 | margin-left:1%;
173 | padding:8px 0px 8px 0px;
174 | }
175 |
176 | li.smallitem:hover {
177 | color:rgba(240, 240, 240, .85);
178 | cursor:pointer;
179 | }
180 |
181 | .bold {
182 | font-weight:bold;
183 | }
184 |
185 | .functionTitle {
186 | margin-top:0px;
187 | margin-bottom:6px;
188 | font-family:Consolas, "Liberation Mono", Menlo, Courier, monospace;
189 | }
190 |
191 | .argument {
192 | margin-top:6px;
193 | font-size:90%;
194 | margin-left:12px;
195 | font-weight:bold;
196 | padding:5px;
197 | }
198 |
199 | .type {
200 | margin-left:-6px;
201 | font-size:90%;
202 | }
203 |
204 | .text {
205 | margin-left:12px;
206 | font-size:90%;
207 | color:rgb(88,93,118);
208 | padding:8px;
209 | }
210 |
211 | .desc {
212 | margin-left:12px;
213 | font-size:90%;
214 | color:rgb(88,93,118);
215 | padding:8px;
216 | margin-bottom:12px;
217 | }
218 |
219 | .string {
220 | color:#87193E;
221 | }
222 |
223 | .array {
224 | color:#6B008F;
225 | }
226 |
227 | .object {
228 | color:#004700;
229 | }
230 |
231 | .number {
232 | color:#0000CC;
233 | }
234 |
235 | .func {
236 | color:#338570;
237 | }
238 |
239 | .divSeparator {
240 | margin-top:-8px;
241 | border-bottom:1px;
242 | border-bottom-color: rgba(200, 200, 200, 0.7 );
243 | border-bottom-style: solid;
244 | }
245 |
246 | .result {
247 | color:gray;
248 | min-height:100px;
249 | max-height:200px;
250 | white-space: pre-wrap;
251 | margin-top:5px;
252 | margin-bottom:10px;
253 | font-size:small;
254 | font-family:Consolas, "Liberation Mono", Menlo, Courier, monospace;
255 | }
256 |
257 | .tall {
258 | max-height:none;
259 | height:300px;
260 | overflow-y:auto;
261 | overflow-x:hidden;
262 | }
263 |
264 | .resultHeader {
265 | color:gray;
266 | font-size:small;
267 | font-family:Consolas, "Liberation Mono", Menlo, Courier, monospace;
268 | }
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
--------------------------------------------------------------------------------
/databases/Contacts.fmp12:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seedcode/fmxj/c845cb165025a536f134ae93a5e8e8ac66ef334c/databases/Contacts.fmp12
--------------------------------------------------------------------------------
/databases/Events.fmp12:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/seedcode/fmxj/c845cb165025a536f134ae93a5e8e8ac66ef334c/databases/Events.fmp12
--------------------------------------------------------------------------------
/examples/deleteRecord.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Create a FileMaker -delete query url by specifying the FileMaker record id. These queries are then passed to the postQueryFMS() function which returns the results as a single object array. The object contains the error code with "0" meaning a succesful delete.
41 |
42 |
43 |
44 | fileName
45 |
46 |
47 | Type:
48 |
49 |
50 | String
51 |
52 |
53 | A String of the name of the hosted FileMaker file.
54 |
55 |
56 |
57 | layoutName
58 |
59 |
60 | Type:
61 |
62 |
63 | String
64 |
65 |
66 | A String of the name of the target layout in the specified file.
67 |
68 |
69 |
70 | recid
71 |
72 |
73 | Type:
74 |
75 |
76 | number
77 |
78 |
The FileMaker Record ID (-recid) of the record to be deleted.
79 |
80 |
81 |
example 1
82 |
Create a -delete query by passing the -recid id to the function. Then POST the query. For this example we'll create a new record to delete (so we don't run out of sample data!)
83 |
84 |
85 |
86 | //create new record query from object
87 | var newRecord = {
88 | "DateStart" : "02/25/2014" ,
89 | "DateEnd" : "02/25/2014" ,
90 | "Description" : "delete example" ,
91 | "Status" : "Open" ,
92 | "Summary" : "test delete example"
93 | } ;
94 | var query = fmxj.editRecordURL("Events", "Events", newRecord);
95 | fmxj.postQueryFMS(query, writeResult);//POST query
96 | function writeResult (js) { // define handler for results.
97 | var record = js[0];
98 | updateElement("example1",JSON.stringify(js, null, 4));//write new record to element as JSON
99 | if(record["-recid"]){//this is the result for our new record.
100 | var recid = record["-recid"];//retrieve record id so we can delete it
101 | var dq = fmxj.deleteRecordURL("Events", "Events", recid );//create query
102 | fmxj.postQueryFMS(dq, writeResult);//POST query
103 | }
104 | else//this is the result of our delete request so write it to element
105 | {
106 | updateElement("example1", JSON.stringify(js, null, 4), true);
107 | }
108 | };
Create a FileMaker -edit or -new query url from a javascript object. These queries are then passed to the postQueryFMS() function which returns the results as a single object array.
41 |
42 |
43 |
44 | fileName
45 |
46 |
47 | Type:
48 |
49 |
50 | String
51 |
52 |
53 | A String of the name of the hosted FileMaker file.
54 |
55 |
56 |
57 | layoutName
58 |
59 |
60 | Type:
61 |
62 |
63 | String
64 |
65 |
66 | A String of the name of the target layout in the specified file.
67 |
68 |
69 |
70 | editObj
71 |
72 |
73 | Type:
74 |
75 |
76 | Object
77 |
78 |
An Object that specifies the changes to be made to the FileMaker record. Property names represent the FileMaker field names. Specifying a -recid property will edit the specified record with an -edit query. Not specifying the -recid will create a new FileMaker record with a -new query. The -modid property can be optionally specified to ensure you are editing the most current version of that record. See more about using the -mod id in FileMaker's XML CWP Guide
79 |
80 |
81 |
82 |
83 |
example 1
84 |
Create a new FileMaker record by not specifying the -recid in the edit object. If the record creation is succesful on the server, then the record will be returned and converted into an object.
85 |
Specifying a -recid property in an object will create a edit query to edit the fields specified in the object. Retrieve the first record on the Events layout and toggle its status. If the status is open, set it to closed and vice-versa.
104 |
105 |
106 |
107 | // query to find all records, unsorted and return the first one.
108 | var query = fmxj.findRecordsURL("Events", "Events", null, null, 1);
109 | fmxj.postQueryFMS(query,createEdit,null,relay); // Perform request
110 | function createEdit(js){ //define handler for returned record
111 | var recid = js[0]["-recid"]; //retrieve record/object's -recid
112 | var modid = js[0]["-modid"]; //retrieve record/object's -modid
113 | var recordStatus = js[0]["Status"]; //retrieve record/object's status
114 | if ( recordStatus == "Open" ){ //toggle status
115 | recordStatus = "Closed"
116 | }
117 | else {
118 | recordStatus = "Open"
119 | } ;
120 | var editObj = { //create new object to edit the record
121 | "-recid":recid,
122 | "-modid":modid,
123 | "Status":recordStatus,
124 | };
125 | //create query from object
126 | var query = fmxj.editRecordURL("Events", "Events", editObj);
127 | fmxj.postQueryFMS(query,writeResult,null,relay); //POST edit query
128 | function writeResult(js){ //define handler for writing edited object.
129 | updateElement("example2", message, true);
130 | };
131 | };
Create a FileMaker query to return an array of objects containing file names for the target server.
41 |
42 |
43 |
example 1
44 |
Create a -dbnames query to return file names for the server. Will just return filenames that have at least one account with xml privileges. This one usually takes a little longer to get a response from the FileMaker Server for some reason.
45 |
53 | An Array of Objects where each object represents a FileMaker type Find Request, i.e. each property specified within an object represents an AND clause and each object on the array represents an OR clause.
54 |
55 |
56 |
57 | searchTypes
58 |
59 |
60 | Type:
61 |
62 |
63 | Object
64 |
65 |
66 | An Object specifying the standard search types to apply when filtering by the specified property. Supported search types are:
67 |
Create a complex xml -findquery string from an array of javascript objects. These queries are then passed to the postQueryFMS() function which returns the results as an array of JavaScript Objects (JSON).
41 |
42 |
43 |
44 | fileName
45 |
46 |
47 | Type:
48 |
49 |
50 | String
51 |
52 |
53 | A String of the name of the hosted FileMaker file.
54 |
55 |
56 |
57 | layoutName
58 |
59 |
60 | Type:
61 |
62 |
63 | String
64 |
65 |
66 | A String of the name of the target layout in the specified file.
67 |
79 | (Optional) An Array of Objects where each object represents a FileMaker Find Request. If this arguement is not passed then a -findall query will be generated.
80 |
81 |
82 |
83 |
84 | sort
85 |
86 |
87 | Type:
88 |
89 |
90 | Object
91 |
92 |
93 | (Optional) An Object of name value pairs specifying a sort order for the query. Supported properties are field1-fieldn and order1-ordern (see example 3 below).
94 |
106 | (Optional) A Number specifying the maximimum number of parent records to be returned from FileMaker Server. Simmilar to Fetch First in FileMaker's SQL syntax.
107 |
119 | (Optional) A Number specifying the number of records to skip in the found set before starting to return them. Simmilar yo Offset in FileMaker's SQL Syntax.
120 |
121 |
122 |
123 |
124 |
example 1
125 |
Two request find query for the layout "Events" in the file "Events".
126 |
Specify a sort order for the request by passing an object to the optional sort argument. Supprted order values are: ascend, descend and valuelist.
156 |
66 | An Array of Objects to be nested into the above parent objects.
67 |
68 |
69 |
70 | childName
71 |
72 |
73 | Type:
74 |
75 |
76 | String
77 |
78 |
79 | The name of the property of the child array in the parent object.
80 |
81 |
82 |
83 | predicates
84 |
85 |
86 | Type:
87 |
88 |
89 | Object
90 |
91 |
92 | An object specifying the match keys to join parent and child objects. Only equijoin matches supported.
93 |
94 |
parentKey1 - parentKeyn: match key properties in the parent objects.
95 |
childKey1 - childKeyn: match key properties in the child objects.
96 |
97 |
98 |
99 |
100 |
example 1
101 |
Call Contacts and Contact Info in separate calls with no portals on the layouts, then stitch together in JS. Compare to example 4 in the postQueryFMS() section where Contact Info is gathered via Portal. This operation tests a little bit slower overall, but the footprint on the server is smaller, so should scale better with multiple users. You also may not need nest them all at once and right away like in this example.
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).
43 |
44 |
45 |
46 | query
47 |
48 |
49 | Type:
50 |
51 |
52 | String
53 |
54 |
55 | A String of the query to be POSTed to FileMaker Server. These Strings will typically be created by the fmxj URL finctions.
56 |
89 | (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.
90 |
91 |
92 |
93 | phpRelay
94 |
95 |
96 | Type:
97 |
98 |
99 | Object
100 |
101 |
102 | (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:
103 |
120 | "pass" : [optional: FM Account Password]
121 |
122 |
123 |
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.
124 |
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.
125 |
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.
126 |
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.
140 | (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:
141 | {
142 | "id" : {
143 | "idFieldName":"id", // filemaker field name is "id"
144 | "getValue" : function(f){ // method name must be getValue.
145 | var field = this["idFieldName"]; //retrieve our field name
146 | return f(field); //retrieve the filemaker field value
147 | }, // end method
148 | }, // end property
149 | }
150 |
151 | In addition to straight field mapping, you can also define the getValue() method to transform/combine the source data.
152 | {
153 | "allDay" : {
154 | "timeStartFieldName" : "TimeStart", //field for start time,
155 | "getValue" : function(f){
156 | var field = this["timeStartFieldName"];
157 | if(f(field).length){ //we have a start time so this is false
158 | return false;
159 | }
160 | else{ //if empty, event is all day
161 | return true;
162 | };
163 | }, // end getValue method
164 | }, // end property
165 | }
166 |
167 | See example 5 below for more details on this argument usage.
168 |
181 | (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.
182 |
195 | (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.
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
These examples are run from seedcode.com to a remote FileMaker Server (hosted at FoxtailTech) using the php relay.
204 |
205 |
206 |
example 1
207 |
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.
208 |
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.
231 |
232 |
233 |
234 | var sourceLoop = [];
235 | var requests = [ //create requests for query
236 | { "DateStart" : "<=2/28/2014" , "DateEnd" : ">=2/1/2014" },
237 | { "DateStart" : "2/1/2014...2/28/2014" }
238 | ];
239 | var sortObject = {
240 | "field1" : "DateStart" ,
241 | "order1" : "ascend" ,
242 | } ;
243 | var query = fmxj.findRecordsURL("Events", "Events", requests) ; // create query
244 | function onReadyFuntion(js){ //define handler for onready
245 | //callBack will return pages recursively, so concat to our array.
246 | sourceLoop = sourceLoop.concat(js);
247 | document.getElementById("example1") = JSON.stringify(sourceLoop, null, 4);
248 | };
249 | // for 375 results per page (max) we don't need an onProgress handler, so we'll set to null.
250 | fmxj.postQueryFMS(query, onReadyFunction, null, relay, null, 375); //make call
251 |
252 |
253 |
254 |
255 |
256 |
257 |
example 3
258 |
Errors returned as a simple one object array with the Filemaker error code and description.
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.
280 | 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.
281 |
282 |
283 |
284 | var fcObject = {
285 | "id" : {
286 | "idField":"id",
287 | "getValue" : function(f){
288 | var field = this["idField"];
289 | return f(field);
290 | },
291 | },
292 | "title" : {
293 | "titleField":"Summary",
294 | "getValue" : function(f){
295 | var field = this["titleField"];
296 | return f(field);
297 | },
298 | },
299 | "allDay" : {
300 | "timeStartField" : "TimeStart",
301 | "getValue" : function(f){
302 | var field = this["timeStartField"];
303 | if(f(field).length){//we have a start time so this is false
304 | return false;
305 | }
306 | else{
307 | return true;
308 | }
309 | },
310 | },
311 | "start" : {
312 | "timeStartField" : "TimeStart",
313 | "dateStartField" : "DateStart",
314 | "yearFormat" : "MM-DD-YYYY",
315 | "timeFormat" : "HH:mm",
316 | "timezone" : "America/Los_Angeles",
317 | "getValue" : function(f){
318 | var time = this["timeStartField"];
319 | var date = this["dateStartField"];
320 | var zone = this["timezone"];
321 | var yearFormat = this["yearFormat"];
322 | var timeFormat = this["timeFormat"];
323 | var date = moment( f(date) + " " + f(time) , yearFormat + " " + timeFormat );
324 | return date.tz(zone).format();
325 | },
326 | },
327 | "end" : {
328 | "timeEndField" : "TimeEnd",
329 | "dateEndField" : "DateEnd",
330 | "dateStartField" : "DateStart", //if no end date is specified we'll need to use the start date.
331 | "yearFormat" : "MM-DD-YYYY",
332 | "timeFormat" : "HH:mm",
333 | "timezone" : "America/Los_Angeles",
334 | "getValue" : function(f){
335 | var time = this["timeEndField"];
336 | var date = this["dateEndField"];
337 | var sdate = this["dateStartField"];
338 | var zone = this["timezone"];
339 | var yearFormat = this["yearFormat"];
340 | var timeFormat = this["timeFormat"];
341 | //use start date if no end date
342 | if(!f(date)){ var d = f(sdate) } else { var d = f(date)};
343 | if(f(time).length){
344 | d = moment( d + " " + f(time) , yearFormat + " " + timeFormat );
345 | }
346 | else
347 | {
348 | d = moment( d , yearFormat + " " + timeFormat ).add( 1, "days");
349 | };
350 | return d.tz(zone).format();
351 | },
352 | },
353 | "description" : {
354 | "descriptionField":"Description",
355 | "getValue" : function(f){
356 | var field = this["descriptionField"];
357 | return f(field);
358 | },
359 | },
360 | "resource" : {
361 | "resourceField":"Resource",
362 | "getValue" : function(f){
363 | var field = this["resourceField"];
364 | return f(field);
365 | },
366 | },
367 | "status" : {
368 | "statusField":"Status",
369 | "getValue" : function(f){
370 | var field = this["statusField"];
371 | return f(field);
372 | },
373 | },
374 | "contactId" : {
375 | "contactIdFieldName":"id_contact",
376 | "getValue" : function(f){
377 | var field = this["contactIdFieldName"];
378 | return f(field);
379 | },
380 | },
381 | "projectId" : {
382 | "projectIdField":"id_project",
383 | "getValue" : function(f){
384 | var field = this["projectIdField"];
385 | return f(field);
386 | },
387 | },
388 | "fmRecordId" : {
389 | "fmRecordIdField":"-recid",
390 | "getValue" : function(f){
391 | var field = this["fmRecordIdField"];
392 | return f(field);
393 | },
394 | },
395 | "fmModId" : {
396 | "fmModIdField":"-modid",
397 | "getValue" : function(f){
398 | var field = this["fmModIdField"];
399 | return f(field);
400 | },
401 | },
402 | };
403 | //build query and make call passing are fcObject as the resultClass argument
404 | var query = fmxj.findRecordsURL("Events", "Events", requests);
405 | fmxj.postQueryFMS(query, writeResults, writeDownload, relay, fcObject);
Sort an array of objects in JSON format by multiple properties.
41 |
42 |
43 |
44 | sortOrder
45 |
46 |
47 | Type:
48 |
49 |
50 | Object
51 |
52 |
53 | An object specifying the properties by which to sort the array of objects, the order by which to sort them, and the data type to apply to the properties.
54 |
55 |
property1 - propertyn: property names to sort by in order, property 1 is sorted first, then property 2, etc.
56 |
order1 - ordern: ascend or descend, default is ascend if not specified
129 |
130 |
131 |
132 |
189 |
190 |
--------------------------------------------------------------------------------
/fmxj.js:
--------------------------------------------------------------------------------
1 | //fmxj.js a Javascript Object Based approach to FileMaker Custom Web Publishing
2 |
3 | /*
4 |
5 | LICENSE
6 |
7 | (The MIT License)
8 |
9 | Copyright (c) 2015 SeedCode SeedCode.Com
10 |
11 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
16 |
17 | */
18 |
19 |
20 | var fmxj = ( function () {
21 | 'use strict'
22 | return {
23 |
24 | postQueryFMS: postQueryFMS ,
25 | convertXml2js: convertXml2Js,
26 | findRecordsURL: findRecordsURL,
27 | editRecordURL: editRecordURL,
28 | deleteRecordURL: deleteRecordURL,
29 | fileNamesURL: fileNamesURL,
30 | layoutNamesURL: layoutNamesURL,
31 | layoutFieldsURL: layoutFieldsURL,
32 | errorDescription: errorDescription,
33 | filterObjects: filterObjects,
34 | sortObjects: sortObjects,
35 | nestObjects: nestObjects,
36 | }
37 |
38 | //**********FileMaker Server Functions
39 |
40 |
41 |
42 |
43 |
44 |
45 | //function does an httpXMLRequest to the server and returns the results in JSON
46 | //Optional PHP Proxy/Relay information can be passed via the phpRelay object.
47 | //user and pass are provided if you're running your own client side auth routine. (Sent via POST).
48 | //var phpRelay = {"php":"fmxj.php","server":"192.168.1.123","protocol":"http","port":80,"user":"Admin","pass":"1234"};
49 | //resultClass is object for defining result objects, specify new properties and map the source values.
50 | //max will paginate results and recursively return pages to the callBack until all results have been returned.
51 | function postQueryFMS( query , callBackOnReady , callBackOnDownload , phpRelay , resultClass , max , nestPortals ) {
52 |
53 | //if you want nesting related records on or off by default, on for this build
54 | if(nestPortals==null){
55 | nestPortals=false;
56 | };
57 |
58 | //check what query type we are as we'll do custom results for some (delete findany)
59 | //check if we're a delete request as we handle error captryre differently, i.e. return ERRORCODE:0.
60 | var li = query.lastIndexOf("-");
61 | var parm = query.substring(li,query.length);
62 | if (parm!=="-delete"&&parm!=="-findany"){
63 | parm = null;
64 | };
65 |
66 | function internalCallBack(js,utc){
67 | var c = js.length;
68 | if(c===max){//our page is full, so increment skip and send back
69 | skip = skip + max;
70 | query = updateParam(query,"-skip",skip);
71 | // Make New Request
72 | req();
73 | };
74 | callBackOnReady(js,utc);
75 | };
76 |
77 | function req(){
78 |
79 | //define request.
80 | var xmlhttp = new XMLHttpRequest();
81 | xmlhttp.onreadystatechange = function(){
82 | if (xmlhttp.readyState == 4 && xmlhttp.status == 200)
83 | {
84 | var utc = new Date().getTime();
85 | var js = convertXml2Js(xmlhttp.responseXML,parm,resultClass,nestPortals);
86 | xmlhttp = null;
87 | if ( js ) { internalCallBack ( js , utc ) } ;
88 | }
89 | };
90 | if(callBackOnDownload){
91 | xmlhttp.onprogress = function(e){
92 | //e.loaded is the only feedback we get from FMS, but still may be useful.
93 | callBackOnDownload(e.loaded);
94 | }
95 | };
96 | xmlhttp.open( "POST", url , true ) ;
97 | xmlhttp.send(query);
98 |
99 | };
100 |
101 | //function for upgating max and skip in query
102 | if (max){
103 | //does the query argument already have a max?
104 | var m = query.indexOf("-max=");
105 | if(m==true){
106 | query = updateParam(query,"-max",max) //if there's a max parm specified already, we override it.
107 | }
108 | else{
109 | query = query.substring(0,li) + "-max=" + max + "&" + query.substring(li,query.length);
110 | };
111 | //does the query argument already have a skip?
112 | var s = query.indexOf("-skip=");
113 | if(s==true){
114 | var se = query.indexOf("&",s);
115 | var skip = query.substring(s+5,se);
116 | }
117 | else{
118 | li = query.lastIndexOf("-");
119 | var skip = 0;
120 | query = query.substring(0,li) + "-skip=" + skip + "&" + query.substring(li,query.length);
121 | };
122 | };
123 |
124 |
125 |
126 |
127 | //if a proxy object is specified, then we'll send our POST to fmxjRelay.PHP page, so update query value.
128 | //Otherwise we're posting locally right to FMS via XML.
129 | if(phpRelay){
130 |
131 | //default to http if not specified
132 | if (phpRelay["protocol"])
133 | {var proxy = phpRelay["protocol"]}
134 | else {var proxy = "http"};
135 |
136 | //define FMS server location if specified
137 | var server = "";
138 | if (phpRelay["server"])
139 | {server = proxy + "://" + phpRelay["server"]}
140 | else {server = ""};
141 |
142 | //append port if specified
143 | if (phpRelay["port"] && server!==""){
144 | server += ":" + phpRelay["port"] + "/";
145 | }
146 | else if (server==="") {
147 | server = "/";
148 | }
149 | else {
150 | server += "/";
151 | }
152 | ;
153 |
154 | var php = phpRelay["php"];
155 |
156 | // if you're running your own authentication routine you can send via POST and the PHP page will use to auth to FMS.
157 | var user = phpRelay["user"];
158 | var pass = phpRelay["pass"];
159 |
160 | //define target and POST payload
161 | var url = server + php ;
162 | var creds = "&u=" + user + "&p=" + pass;
163 | var lengthParamStr = "&l=";
164 | var cl = creds.length;
165 | var cl = cl.toString().length + lengthParamStr.length + cl;
166 | query += creds + lengthParamStr + cl;
167 |
168 | }
169 | //going right to FMS via XML locally.
170 | else{
171 | url = "/fmi/xml/FMPXMLRESULT.xml";
172 | };
173 |
174 | req();
175 |
176 | };
177 |
178 | //parses a FMPXMLRESULT into JSON adding -recid and -modid properties.
179 | //FMPXMLRESULT is 50-60% the size of fmresultset, so that's what we're using.
180 | //function for converting xml onready in queryFMS but could have uses outside of there, so leaving Public
181 | function convertXml2Js( xml , requestType , resultClass , portal ){
182 |
183 | //rather than parsing errors.
184 | if(!xml){return
185 | [{"ERRORCODE" : "-1","DESCRIPTION" : "No XML Results from the FileMaker Server"}]
186 | };
187 |
188 | var c = 0;
189 | var v = 0;
190 | var dataTag = "DATA";
191 | var colTag = "COL";
192 | var resultTag = "RESULTSET" ;
193 | var errorCodeTag = "ERRORCODE" ;
194 | var description = "DESCRIPTION"
195 | var metadataTag = "METADATA" ;
196 | var dataBaseTag = "DATABASE" ;
197 | var dateFormatTag = "DATEFORMAT" ;
198 | var timeFormatTag = "TIMEFORMAT" ;
199 | var recordTag = "ROW" ;
200 | var foundTag = "FOUND";
201 | var recid = "RECORDID";
202 | var modid = "MODID";
203 | var id = "";
204 | var mid = "";
205 | var row = "";
206 | var result = [];
207 | var obj = {};
208 | var newArray = [];
209 |
210 | //function for generating layout/field info from the METADATA Node
211 | //returns an object with two objects and an array:
212 | //model{}: fieldnames reference index position
213 | //fields{}: fieldnames reference data type
214 | //index[]: fieldnames (no TO:: pre-fix) in their index position
215 | function layoutModel ( FMXMLMetaData ){
216 |
217 | var index = [];
218 | var result = {}
219 | var fields = {};
220 | var model = {};
221 | var nameTag = "NAME";
222 | var typeTag = "TYPE";
223 | var field = "";
224 | var type = "";
225 | var fieldCount = FMXMLMetaData.childNodes.length ;
226 | var i = 0;
227 |
228 | while(i0 && portal===true){
233 | var pos = field.indexOf("::");
234 | var t = field.substring(0,pos);
235 | var f = field.substring(pos+2);
236 | t = "-" + t; //add hyphen prefix to keep from coliding with parent fields
237 | //does this proprty t exist in our model
238 | if(!model[t]){model[t]=[]};
239 | model[t].push(i);
240 | index[i]=f;
241 | }
242 | //local field
243 | else
244 | {
245 | model[field] = [i];
246 | index[i]=field;
247 | }
248 | fields[field]=type;
249 | i++;
250 | };
251 | result["model"]=model;
252 | result["fields"]=fields;
253 | result["index"]=index;
254 | return result;
255 | };
256 |
257 | //retrieve value from the current XML DATA node by field name
258 | //returns an array as there can be 1...n values
259 | function valueByField(field){
260 | //can return a string or an array.
261 | if(field==="-recid"){return id};
262 | if(field==="-modid"){return mid};
263 | var a = fieldObjects["model"][field];
264 | if(a.length>1&&field.indexOf("-")===0){ //fields in a portal
265 | var i = 0;
266 | var result = [];
267 | for (i in a){
268 | result.push(valueByIndex(a[i]))
269 | };
270 | return result;
271 | }
272 | else{
273 | return valueByIndex(a[0]);
274 | }
275 | };
276 |
277 | //retrieve value from the current XML DATA node by field name
278 | //returns a string if there is just one value.
279 | function valueByFieldString(field){
280 | //can return a string or an array.
281 | if(field==="-recid"){return id};
282 | if(field==="-modid"){return mid};
283 | var a = fieldObjects["model"][field];
284 | if(a.length>1){ //fields in a portal
285 | var i = 0;
286 | var result = [];
287 | for (i in a){
288 | result.push(valueByIndex(a[i]))
289 | };
290 | return result;
291 | }
292 | else{
293 | return valueByIndex(a[0])[0];
294 | }
295 | };
296 |
297 | //retrieve value from the current XML DATA node by field index
298 | function valueByIndex(i){
299 | //Just get the values by index
300 | //return as an array as we may have multiples.
301 | var column = row.getElementsByTagName(colTag)[i];
302 | if(!column){
303 | return ""
304 | }
305 | var children = column.childNodes.length;
306 | //override the count if portal is set to false so we just get the first value
307 | if(!portal){children=1};
308 | var c = 0;
309 | var data = "";
310 | var val = "";
311 | var result = [];
312 | while (c
594 | function errorDescription(code){
595 | var obj =
596 | {
597 | "-2" : "Couldn't connect to the FileMaker Server. Check you user name and password, and check you server configuration",
598 | "-1" : "Unknown error",
599 | 0 : "No error",
600 | 1 : "User canceled action",
601 | 2 : "Memory error",
602 | 3 : "Command is unavailable (for example, wrong operating system, wrong mode, etc.)",
603 | 4 : "Command is unknown",
604 | 5 : "Command is invalid (for example, a Set Field script step does not have a calculation specified)",
605 | 6 : "File is read-only",
606 | 7 : "Running out of memory",
607 | 8 : "Empty result",
608 | 9 : "Insufficient privileges",
609 | 10 : "Requested data is missing",
610 | 11 : "Name is not valid",
611 | 12 : "Name already exists",
612 | 13 : "File or object is in use",
613 | 14 : "Out of range",
614 | 15 : "Can't divide by zero",
615 | 16 : "Operation failed, request retry (for example, a user query)",
616 | 17 : "Attempt to convert foreign character set to UTF-16 failed",
617 | 18 : "Client must provide account information to proceed",
618 | 19 : "String contains characters other than A-Z, a-z, 0-9 (ASCII)",
619 | 100 : "File is missing",
620 | 101 : "Record is missing",
621 | 102 : "Field is missing",
622 | 103 : "Relationship is missing",
623 | 104 : "Script is missing",
624 | 105 : "Layout is missing",
625 | 106 : "Table is missing",
626 | 107 : "Index is missing",
627 | 108 : "Value list is missing",
628 | 109 : "Privilege set is missing",
629 | 110 : "Related tables are missing",
630 | 111 : "Field repetition is invalid",
631 | 112 : "Window is missing",
632 | 113 : "Function is missing",
633 | 114 : "File reference is missing",
634 | 130 : "Files are damaged or missing and must be reinstalled",
635 | 131 : "Language pack files are missing (such as template files)",
636 | 200 : "Record access is denied",
637 | 201 : "Field cannot be modified",
638 | 202 : "Field access is denied",
639 | 203 : "No records in file to print, or password doesn't allow print access",
640 | 204 : "No access to field(s) in sort order",
641 | 205 : "User does not have access privileges to create new records; import will overwrite existing data",
642 | 206 : "User does not have password change privileges, or file is not modifiable",
643 | 207 : "User does not have sufficient privileges to change database schema, or file is not modifiable",
644 | 208 : "Password does not contain enough characters",
645 | 209 : "New password must be different from existing one",
646 | 210 : "User account is inactive",
647 | 211 : "Password has expired",
648 | 212 : "Invalid user account and/or password. Please try again",
649 | 213 : "User account and/or password does not exist",
650 | 214 : "Too many login attempts",
651 | 215 : "Administrator privileges cannot be duplicated",
652 | 216 : "Guest account cannot be duplicated",
653 | 217 : "User does not have sufficient privileges to modify administrator account",
654 | 300 : "File is locked or in use",
655 | 301 : "Record is in use by another user",
656 | 302 : "Table is in use by another user",
657 | 303 : "Database schema is in use by another user",
658 | 304 : "Layout is in use by another user",
659 | 306 : "Record modification ID does not match",
660 | 400 : "Find criteria are empty",
661 | 401 : "No records match the request",
662 | 402 : "Selected field is not a match field for a lookup",
663 | 403 : "Exceeding maximum record limit for trial version of FileMaker Pro",
664 | 404 : "Sort order is invalid",
665 | 405 : "Number of records specified exceeds number of records that can be omitted",
666 | 406 : "Replace/Reserialize criteria are invalid",
667 | 407 : "One or both match fields are missing (invalid relationship)",
668 | 408 : "Specified field has inappropriate data type for this operation",
669 | 409 : "Import order is invalid",
670 | 410 : "Export order is invalid",
671 | 412 : "Wrong version of FileMaker Pro used to recover file",
672 | 413 : "Specified field has inappropriate field type",
673 | 414 : "Layout cannot display the result",
674 | 500 : "Date value does not meet validation entry options",
675 | 501 : "Time value does not meet validation entry options",
676 | 502 : "Number value does not meet validation entry options",
677 | 503 : "Value in field is not within the range specified in validation entry options",
678 | 504 : "Value in field is not unique as required in validation entry options",
679 | 505 : "Value in field is not an existing value in the database file as required in validation entry options",
680 | 506 : "Value in field is not listed on the value list specified in validation entry option",
681 | 507 : "Value in field failed calculation test of validation entry option",
682 | 508 : "Invalid value entered in Find mode",
683 | 509 : "Field requires a valid value",
684 | 510 : "Related value is empty or unavailable",
685 | 511 : "Value in field exceeds maximum number of allowed characters",
686 | 600 : "Print error has occurred",
687 | 601 : "Combined header and footer exceed one page",
688 | 602 : "Body doesn't fit on a page for current column setup",
689 | 603 : "Print connection lost",
690 | 700 : "File is of the wrong file type for import",
691 | 706 : "EPSF file has no preview image",
692 | 707 : "Graphic translator cannot be found",
693 | 708 : "Can't import the file or need color monitor support to import file",
694 | 709 : "QuickTime movie import failed",
695 | 710 : "Unable to update QuickTime file reference because the database file is read-only",
696 | 711 : "Import translator cannot be found",
697 | 714 : "Password privileges do not allow the operation",
698 | 715 : "Specified Excel worksheet or named range is missing",
699 | 716 : "A SQL query using DELETE, INSERT, or UPDATE is not allowed for ODBC import",
700 | 717 : "There is not enough XML/XSL information to proceed with the import or export",
701 | 718 : "Error in parsing XML file (from Xerces)",
702 | 719 : "Error in transforming XML using XSL (from Xalan)",
703 | 720 : "Error when exporting; intended format does not support repeating fields",
704 | 721 : "Unknown error occurred in the parser or the transformer",
705 | 722 : "Cannot import data into a file that has no fields",
706 | 723 : "You do not have permission to add records to or modify records in the target table",
707 | 724 : "You do not have permission to add records to the target table",
708 | 725 : "You do not have permission to modify records in the target table",
709 | 726 : "There are more records in the import file than in the target table. Not all records were imported",
710 | 727 : "There are more records in the target table than in the import file. Not all records were updated",
711 | 729 : "Errors occurred during import. Records could not be imported",
712 | 730 : "Unsupported Excel version. (Convert file to Excel 7.0 (Excel 95), Excel 97, 2000, or XP format and try again)",
713 | 731 : "The file you are importing from contains no data",
714 | 732 : "This file cannot be inserted because it contains other files",
715 | 733 : "A table cannot be imported into itself",
716 | 734 : "This file type cannot be displayed as a picture",
717 | 735 : "This file type cannot be displayed as a picture. It will be inserted and displayed as a file",
718 | 800 : "Unable to create file on disk",
719 | 801 : "Unable to create temporary file on System disk",
720 | 802 : "Unable to open file",
721 | 803 : "File is single user or host cannot be found",
722 | 804 : "File cannot be opened as read-only in its current state",
723 | 805 : "File is damaged; use Recover command",
724 | 806 : "File cannot be opened with this version of FileMaker Pro",
725 | 807 : "File is not a FileMaker Pro file or is severely damaged",
726 | 808 : "Cannot open file because access privileges are damaged",
727 | 809 : "Disk/volume is full",
728 | 810 : "Disk/volume is locked",
729 | 811 : "Temporary file cannot be opened as FileMaker Pro file",
730 | 813 : "Record Synchronization error on network",
731 | 814 : "File(s) cannot be opened because maximum number is open",
732 | 815 : "Couldn't open lookup file",
733 | 816 : "Unable to convert file",
734 | 817 : "Unable to open file because it does not belong to this solution",
735 | 819 : "Cannot save a local copy of a remote file",
736 | 820 : "File is in the process of being closed",
737 | 821 : "Host forced a disconnect",
738 | 822 : "FMI files not found; reinstall missing files",
739 | 823 : "Cannot set file to single-user, guests are connected",
740 | 824 : "File is damaged or not a FileMaker file",
741 | 900 : "General spelling engine error",
742 | 901 : "Main spelling dictionary not installed",
743 | 902 : "Could not launch the Help system",
744 | 903 : "Command cannot be used in a shared file",
745 | 904 : "Command can only be used in a file hosted under FileMaker Server",
746 | 905 : "No active field selected; command can only be used if there is an active field",
747 | 920 : "Can't initialize the spelling engine",
748 | 921 : "User dictionary cannot be loaded for editing",
749 | 922 : "User dictionary cannot be found",
750 | 923 : "User dictionary is read-only",
751 | 951 : "An unexpected error occurred (*)",
752 | 954 : "Unsupported XML grammar (*)",
753 | 955 : "No database name (*)",
754 | 956 : "Maximum number of database sessions exceeded (*)",
755 | 957 : "Conflicting commands (*)",
756 | 958 : "Parameter missing (*)",
757 | 1200 : "Generic calculation error",
758 | 1201 : "Too few parameters in the function",
759 | 1202 : "Too many parameters in the function",
760 | 1203 : "Unexpected end of calculation",
761 | 1204 : "Number, text constant, field name or \"(\" expected",
762 | 1205 : "Comment is not terminated with \"*\/\"",
763 | 1206 : "Text constant must end with a quotation mark",
764 | 1207 : "Unbalanced parenthesis",
765 | 1208 : "Operator missing, function not found or \"(\" not expected",
766 | 1209 : "Name (such as field name or layout name) is missing",
767 | 1210 : "Plug-in function has already been registered",
768 | 1211 : "List usage is not allowed in this function",
769 | 1212 : "An operator (for example, +, -, *) is expected here",
770 | 1213 : "This variable has already been defined in the Let function",
771 | 1214 : "AVERAGE, COUNT, EXTEND, GETREPETITION, MAX, MIN, NPV, STDEV, SUM and GETSUMMARY: expression found where a field alone is needed",
772 | 1215 : "This parameter is an invalid Get function parameter",
773 | 1216 : "Only Summary fields allowed as first argument in GETSUMMARY",
774 | 1217 : "Break field is invalid",
775 | 1218 : "Cannot evaluate the number",
776 | 1219 : "A field cannot be used in its own formula",
777 | 1220 : "Field type must be normal or calculated",
778 | 1221 : "Data type must be number, date, time, or timestamp",
779 | 1222 : "Calculation cannot be stored",
780 | 1223 : "The function referred to does not exist",
781 | 1400 : "ODBC driver initialization failed; make sure the ODBC drivers are properly installed",
782 | 1401 : "Failed to allocate environment (ODBC)",
783 | 1402 : "Failed to free environment (ODBC)",
784 | 1403 : "Failed to disconnect (ODBC)",
785 | 1404 : "Failed to allocate connection (ODBC)",
786 | 1405 : "Failed to free connection (ODBC)",
787 | 1406 : "Failed check for SQL API (ODBC)",
788 | 1407 : "Failed to allocate statement (ODBC)",
789 | 1408 : "Extended error (ODBC)"
790 |
791 | };
792 | return obj[code];
793 | };
794 |
795 | //**********Javascript Object Array Functions
796 |
797 | //Filter an Array of objects by property.
798 | //returns a new object array based on filters
799 | //each "filters" object represents an OR clause containing AND clauses, example:
800 | //[{"Status":"Active","Resource":"Room A"},{"Status":"New","resource":"Room A"}]
801 | //returns all objects where the resource is "Room A" and the status is "Active" or "New".
802 | //specify data types as a single object: {"Status":"String"} unspecified properties will be treated as strings.
803 | function filterObjects( filters , searchTypes , source ) {
804 |
805 | //if no filters applied, we can just return the source
806 | if(!filters){return source};
807 |
808 | //regExp logic is here: for string, use "begins with" by default.
809 | var generateRegEx = function ( v , dt ) {
810 |
811 | var re = new RegExp();
812 | if (dt.toUpperCase() === "STARTSWITH") {
813 | re = new RegExp ( "^" + v + "|\\s" + v , "mi" ) ;
814 | }
815 | else if (dt.toUpperCase() === "EQUALS") {
816 | re = new RegExp ( "^" + v + "$" ) ;
817 | }
818 | else if (dt.toUpperCase() === "CONTAINS") {
819 | re = new RegExp ( "\\" + v + "\\" , "mi" ) ;
820 | };
821 | return re;
822 | };
823 |
824 | var s = 0;
825 | var f = 0;
826 | var accept = 0;
827 | var numFilterFields = 0;
828 | var ff = 0;
829 |
830 | var thisFilterField = "";
831 | var thisFilterFieldValue = "";
832 | var thisObjectFieldValue = "";
833 | var thisFilterFieldType = "";
834 |
835 | var newObjectArray = [];
836 | var filterFields = [] ;
837 |
838 | var thisObject = {};
839 | var thisFilter = {};
840 |
841 | var thisRE = new RegExp();
842 | //loop through objects
843 | for ( s in source ) {
844 | thisObject = source[s];
845 | //loop through filters and test accordingly.
846 | for ( f in filters ) {
847 | accept = 0;
848 | thisFilter = filters[f];
849 | filterFields = Object.keys ( thisFilter ) ;
850 | numFilterFields = filterFields.length ;
851 | for ( ff in filterFields ){
852 | thisFilterField = filterFields[ff];
853 | thisFilterFieldValue = thisFilter[thisFilterField];
854 | thisObjectFieldValue = thisObject[thisFilterField];
855 | thisFilterFieldType = searchTypes[thisFilterField];
856 | if ( !thisFilterFieldType ) {
857 | thisFilterFieldType = "STARTSWITH"
858 | };
859 | thisRE = generateRegEx ( thisFilterFieldValue , thisFilterFieldType );
860 | if ( thisRE.test ( thisObjectFieldValue)) {
861 | accept ++ ;
862 | };
863 | };
864 | //add to new object if all filter fields in the filter return true for our regExp
865 | if ( accept === numFilterFields ) {
866 | newObjectArray.push(thisObject);
867 | break;
868 | } ;
869 | };
870 |
871 | };
872 |
873 | return newObjectArray ;
874 | };
875 |
876 | //Sort an Array of objects by property.
877 | //the "sortOrder" is a single object specifying properties, order and type
878 | //{"property1":"StartDate","order1":"Ascend","type1":"date","property2":"Resource","order2":"Descend","type2":"string"}
879 | //Supported types: String, Number, Date, Time
880 | function sortObjects( sortOrder, source ) {
881 |
882 | var convertDateString = function ( ds ) {var d = new Date(ds);return(Number(d));};
883 | var convertTimeString = function ( ts ) {
884 | var vals = ts.split(':');
885 | var v = 0;
886 | var thisVal = 0;
887 | var seconds = 0;
888 | for ( v in vals ){
889 | thisVal = Number ( vals[v].substr(0,2) );
890 | seconds += thisVal * Math.pow(60,2-v);
891 | }
892 | return seconds;
893 | };
894 |
895 | var scEvalSort = function ( s , t , dType , sOrder ){
896 |
897 | if ( dType.toUpperCase() === "STRING" ){
898 | if ( sOrder.toUpperCase() === "ASCEND" ) {
899 | if ( s < t ) { return -1 } else { return 1 } ;
900 | }
901 | else {
902 | if ( s > t ) { return -1 } else { return 1 } ;
903 | };
904 | }
905 | else if ( dType.toUpperCase() === "NUMBER" ) {
906 | if ( sOrder.toUpperCase() === "ASCEND" ) {
907 | return s - t }
908 | else {
909 | return t - s } ;
910 | }
911 | else {
912 | return 0
913 | };
914 | };
915 |
916 | //var sortFields = Object.keys ( sortOrder ) ;
917 |
918 | source.sort ( function ( a , b ) {
919 |
920 | var result = 0;
921 | var sf = 0;
922 | var i = 0;
923 |
924 | var adp = "";
925 | var v = "";
926 | var p = "";
927 | var t = "";
928 | var ad = "";
929 |
930 |
931 | for ( sf in sortOrder ) {
932 | i++
933 | p = sortOrder["property"+i];
934 | ad = sortOrder["order"+i];
935 | t = sortOrder["type"+i];
936 |
937 | if (!t){t="STRING"};
938 |
939 | if (!a[p]) { var x = "" } else { var x = a[p] } ;
940 | if (!b[p]) { var y = "" } else { var y = b[p] };
941 |
942 |
943 | //ignore case
944 | if ( t.toUpperCase() === "STRING" ) {
945 | if ( x.length > 0 ) { var x = x.toUpperCase()} else { var x = "" } ;
946 | if ( y.length > 0 ) { var y = y.toUpperCase()} else { var y = "" } ;
947 | } ;
948 |
949 | //convert dates to numbers for sort.
950 | if ( t.toUpperCase() === "DATE" ) {
951 | if ( x.length > 0 ) { x = convertDateString(x);} else { var x = "" } ;
952 | if ( y.length > 0 ) { y = convertDateString(y);} else { var y = "" } ;
953 | t = "NUMBER";
954 | } ;
955 |
956 | //convert dates to numbers for sort.
957 | if ( t.toUpperCase() === "TIME" ) {
958 | if ( x.length > 0 ) { x = convertTimeString(x);} else { var x = "" } ;
959 | if ( y.length > 0 ) { y = convertTimeString(y);} else { var y = "" } ;
960 | t = "NUMBER";
961 | } ;
962 |
963 | if ( x !== y ) {
964 | result = sf ;
965 | break;
966 | };
967 | };
968 |
969 | if ( result === 0 ) {
970 | return 0 ;
971 | }
972 | else {
973 | if ( length.ad === 0 ) ad = "ASCEND";
974 | return scEvalSort ( x , y , t , ad ) ;
975 | } ;
976 | }
977 | );
978 | } ;
979 |
980 | //nest one object array (child) into another (parent) using SQL like predicates
981 | //childName will be the property name of the nested array.
982 | //predicates object will look like { "parentKey1" : "contactID", "childKey1" : "contactID" , "operator1" : "=" };
983 | //additonal predicates ban ce assigned as leftKey2, leftKey3, etc.
984 | function nestObjects( parentArray, childArray, childName, predicates) {
985 |
986 | function nestTheseObjects(p,c){
987 | var n = 1;
988 | var ok = 1;
989 |
990 | while ( predicates["parentKey" + n] ) {
991 | var pv = p[predicates["parentKey" + n]];
992 | var cv = c[predicates["childKey" + n]];
993 | if (pv===cv){ok++}
994 | n++;
995 | };
996 | if(n===ok&&n>1){ // all predicates match for this object, so we can nest.
997 | //does the childName already exist in this Parent?
998 | //if not, create it.
999 | if(!p[childName]){
1000 | p[childName]=[]
1001 | };
1002 | p[childName].push(c);
1003 | return true;
1004 | }
1005 | };
1006 |
1007 | for (var pa in parentArray){
1008 | var cc = [];
1009 | var i = 0;
1010 | var result = [];
1011 | for (var ca in childArray){
1012 | result = nestTheseObjects(parentArray[pa],childArray[ca]);
1013 | if (result){cc.unshift(ca)} //mark this object for removal from child array.
1014 | };
1015 | for (i in cc){
1016 | childArray.splice(cc[i],1); //remove the child records that were nested from the childArray, so next pass a little shorter.
1017 | };
1018 | };
1019 | childArray=null;
1020 | };
1021 |
1022 |
1023 | //**********Private Functions
1024 |
1025 | //updates a parameter value in url query and returns new query string
1026 | function updateParam(query, param, value){
1027 | var pl = param.length;
1028 | var ql = query.length;
1029 | var pp = query.lastIndexOf(param);
1030 | if(!pp){return} //can only update existing params
1031 | var pe = query.indexOf("&",pp);
1032 | var q = query.substring( 0, Number(pp) + Number(pl) + 1 ) + value + query.substring(pe,ql);
1033 | return q;
1034 | };
1035 |
1036 |
1037 |
1038 | }())
1039 |
1040 |
1041 |
1042 |
--------------------------------------------------------------------------------
/fmxjDemo.js:
--------------------------------------------------------------------------------
1 | //accompanying js file for fmxj demo html file.
2 | //dependencies: fmxj.js. fmxj.css
3 |
4 |
5 |
6 |
7 | //define PHP relay info if using it, otherwise set to null
8 | //var relay = null;
9 | //Credentials are not being passed here, but are hardcoded into the PHP file.
10 | //If running your own authentication routine in JS you can add u an p properties to this object
11 | //They will be sent via POST to your PHP page, which will use them to authenticate to FMS
12 | //This is the SeedCode Test Server so be kind to it!!
13 | var relay = {"php":"fmxjRelay.php","server":"sc-fms13-fms.fmsdb.com","protocol":"https","port":"443"};
14 | //var relay = {"php":"fmxjRelay.php","server":"192.168.1.10"};
15 |
16 |
17 | //***************FUNCTIONS*********************
18 |
19 | function createMessage(js, utc, start, num){
20 | var end = new Date().getTime();
21 | var dlc = utc - start;
22 | var cc = end - utc;
23 | var tt = end - start;
24 | var total = js.length;
25 | if(!total){total = 1};
26 | if(!num){var num= total};
27 | var message = "" + total +
28 | " FileMaker records downloaded in " + dlc + " milliseconds\n" +
29 | "" +
30 | "FMPXMLRESULT converted to JS objects in " + cc + " milliseconds\n" +
31 | "Displaying the first " + num + " \"stringified\" objects.\n" +
32 | "" + (end - start) + " total milliseconds.\n\n";
33 | return message;
34 |
35 | };
36 |
37 | function createMessageShort(js, utc, start, num){
38 | var end = new Date().getTime();
39 | var dlc = utc - start;
40 | var cc = end - utc;
41 | var tt = end - start;
42 | var total = js.length;
43 | if(!total){total = 1};
44 | if(!num){var num= total};
45 | var message = "•" + total +
46 | " FileMaker records downloaded in " + dlc + " milliseconds\n" +
47 | "" +
48 | "FMPXMLRESULT converted to JS objects in " + cc + " milliseconds\n" +
49 | "Displaying the first " + num + " records as \"stringified\" objects.\n"
50 | return message;
51 |
52 | };
53 |
54 | function createDisplay(js, utc, start, num){
55 | var indent = 4; // JSON indent
56 | var display = JSON.stringify(js.slice(0,num), null, indent);
57 | var message = createMessage(js,utc,start,num);
58 | return message+display ;
59 | };
60 |
61 | function updateElement(id, value, append){
62 | if(append){document.getElementById(id).innerHTML += value;}
63 | else{document.getElementById(id).innerHTML = value;}
64 | };
65 |
66 | function editQuery(source){
67 | var firstRecord = source[0];
68 | var recid = firstRecord["-recid"];
69 | var recordStatus = firstRecord["Status"] ;
70 | var recordModId = firstRecord["-modid"] ;
71 | //create edit object and requests array
72 | var edit = {"-recid":recid};
73 | var requests = [edit];
74 | //toggle status
75 | if ( recordStatus == "Open" ) {var newStatus = "Closed" } else {var newStatus = "Open" } ;
76 | edit["Status"] = newStatus ;
77 | edit["-modid"] = recordModId ;
78 | return fmxj.editRecordURL( "Events" , "Events" , edit ) ;
79 | };
80 |
81 | //***************Load Sidebar*********************
82 |
83 | updateElement("sb",'
90 | Do the data interchange work with JavaScript.
91 |
92 |
93 |
Build complex queries and perform HTPP POSTs to your FileMaker Server
94 |
95 |
Return FileMaker parent and child records as JavaScript Objects/JSON
96 |
97 |
Create, edit and delete FileMaker records with JavaScript objects
98 |
99 |
Filter and sort Javascript objects locally with complex criteria.
100 |
101 |
102 |
103 | Query strings are created from JavaScript Objects and then sent as an HTTP POST to FileMaker's XML Web Publishing Engine. An XML FMPXMLRESULT is returned and converted into JavaScript Objects/JSON by fmxj.
104 |
105 | HTTP POSTS can be done directly to the FileMaker Server's XML WPE or a simple PHP relay can be used to get around cross-domain issues and provide more authentication options. See more on the PHP relay in the postQueryFMS() function.
106 |
107 |
108 |
Example
109 |
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 to the postQueryFMS() function. The required handler for onreadystateexchange is defined as well as the optional onprogress handler.
110 |
111 |
112 |
113 | var requests = [ //create find requests for query. each object is request.
114 | { "DateStart" : "<=2/28/2014" , "DateEnd" : ">=2/1/2014" },
115 | { "DateStart" : "2/1/2014...2/28/2014" }
116 | ];
117 | var query = fmxj.findRecordsURL("Events", "Events", requests) ; // create query
118 |
119 | function onReadyFuntion(js){ //define handler for onready
120 | document.getElementById("example1") = JSON.stringify(js, null, 4);
121 | } ;
122 | function onProgressFuntion(n){ //define handler for onprogress
123 | document.getElementById("example1") += n + " bytes downloaded\n";
124 | } ;
125 | fmxj.postQueryFMS(query, onReadyFunction, onProgressFunction); //make call