├── .gitignore ├── LICENSE ├── README.md ├── examples └── simple.js ├── index.js ├── package.json ├── src ├── FMSJSError.js ├── FileMakerServerError.js ├── createLayout.js ├── createRequest.js ├── database.js ├── dbNamesRequest.js ├── filemakerserver.js └── old.js └── test ├── ContactsTest.fmp12 ├── bootstrap.js ├── config-default.js ├── mocha.opts └── unit ├── fmsTest.js └── layoutTest.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | .idea 30 | 31 | /test/config.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2015 Todd geist 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fms-js 2 | node and browser connection for FileMaker Server 3 | 4 | ### Status: 2 - unstable 5 | The api is under development and may change. Backwards compatibility will be maintained if feasible. See [node stability ratings](https://gist.github.com/isaacs/1776425) 6 | 7 | ### Installation 8 | fms-js can be installed using npm 9 | 10 | `npm install fms-js` 11 | 12 | ### Usage 13 | Once installed you can require the fms object, and start to use it 14 | 15 | ```javascript 16 | var fms = require('fms-js'); 17 | 18 | // create a connection object 19 | 20 | var connection = fms.connection({ 21 | url : '', 22 | userName : 'username', 23 | password : 'password' 24 | }); 25 | 26 | //use the connection object to create requests 27 | var listDBNamesRequest = connection.dbnames(); 28 | 29 | //and send it to FileMaker Server 30 | //all request are asynchronous. 31 | //Pass the callback to the 'send()' method 32 | 33 | listDBNamesRequest.send(callback) 34 | 35 | ``` 36 | 37 | The API is chainable. So you can do some fun expressive things. The following will find the first 10 contacts who have the firstName 'todd" 38 | 39 | ```javascript 40 | connection 41 | .db('Contacts') 42 | .layout('contacts') 43 | .find({ 44 | 'firstName' : 'todd' 45 | }) 46 | .set('-max', 10) 47 | .send(callback) 48 | 49 | ``` 50 | You can keep a reference to any point in the chain. So this is the equivilent of the above 51 | 52 | ```javascript 53 | var db = connection.db('Contacts'); 54 | var layout = db.layout('contacts'); 55 | var findRequest = layout.find({ 56 | 'firstName' : 'todd' 57 | }) 58 | var findRequestFirstTen = findRequest.set('-max', 10) 59 | findRequestFirstTen.send(callback) 60 | ``` 61 | 62 | ### General Concepts 63 | fms-js uses the same commands and parameters as the XML Gateway provided by the FileMaker Server. They are just exposed in a more fluent chainable way. 64 | 65 | The API creates requests which you then send to the server 66 | 67 | All requests made to the FileMaker Server are asynchronous. That means they will take a 'callback'. The callback gets two parameters, the first is an error, the second is the result. This is standard node callback style. 68 | 69 | Example: Get all script names from the contactsTest db. 70 | 71 | ```javascript 72 | connection 73 | .db('contactTest') 74 | .scriptnames() 75 | .send(function(err, result){ 76 | //err will be null on a succesful request 77 | //err will contain an Error when it fails 78 | 79 | //result be null on error 80 | //result will contains the request result on success 81 | }) 82 | ``` 83 | ### API 84 | 85 | ##### fms.connection( {options} ) 86 | 87 | * Options 88 | 89 | * url - the url or ip address of the server 90 | * username - the userName 91 | * password - the password 92 | 93 | * Returns 94 | * a connection object 95 | 96 | 97 | ##### connection.dbnames() 98 | 99 | * No parameters 100 | * returns a request for all database names on the server. Send it with .send() 101 | 102 | ##### connection.db(databasename) 103 | 104 | * Takes the database name 105 | * Returns a DB object 106 | 107 | ##### db.scriptnames() 108 | * No parameters 109 | * returns a request for all script names in the file. Send it with .send() 110 | 111 | ##### db.layoutnames() 112 | * No parameters 113 | * returns a request for all layouts in the file. Send it with .send() 114 | 115 | ##### db.layout(layoutname) 116 | 117 | * Takes the layout name 118 | * Returns a Layout object 119 | 120 | ##### layout.query(options) 121 | * takes an options object containing the name values pairs from the xml gateway 122 | * returns a request object. send it with .send() 123 | 124 | This can be used to perform any layout based request. You provide the name value pairs in the form of an object. For example this object 125 | 126 | ```javascript 127 | { 128 | '-scriptname' : "RunOnFoundSet" 129 | '-max' : 100 130 | '-findall' : '' 131 | } 132 | ``` 133 | would return an Request Object that would return the first 100 records on the layout, and a run a script called "RunOnFoundSet". For more information on what options are available see the XML Web Publishing Guide for FileMaker Server. 134 | 135 | The rest of the API provides short hand methods on top of this query 136 | 137 | ##### layout.find({criteria}) 138 | * takes an object with field name and field value pairs 139 | * returns a request object. send it with .send 140 | 141 | ##### layout.findall({options}) 142 | * takes an options object with any of the query modifiers like '-max', '-sort' 143 | * returns a request object that finds all records on the layout. send it with .send 144 | 145 | ##### layout.findany({options}) 146 | * takes an options object with any of the query modifiers like '-max', '-sort' 147 | * returns a request object that finds a random record. send it with .send 148 | 149 | ##### layout.create({options}) 150 | * takes an options object with all data and query options 151 | * returns a request object that will create a record. send it with .send 152 | 153 | ##### layout.edit({options}) 154 | * takes an options object with all data and query options. IT MUST include the recid field 155 | * returns a request object that will edit a record. send it with .send 156 | 157 | ##### layout.delete(recid) 158 | * recid is FileMakers internal record id 159 | * returns a request object that will delete the record. send it with .send 160 | 161 | ##### request.set(name, value) 162 | * parmaters 163 | * name - the name of the option to set 164 | * value - the value to set it to 165 | * Returns 166 | * the request object 167 | 168 | you can use this to modify a request before sending it. It will override any options that were sent in before. For example, give a findall request in the variable 'request' 169 | 170 | ```javascript 171 | request 172 | .set('-max', 100) 173 | .set('-sortfield', 'lastName') 174 | .set('-sortorder, 'ascend' ) 175 | .send(callback) 176 | ``` 177 | would set the max returned to 100, and the sort to lastName ascending, before sending it with .send(callback). This gives you another more fluent way of building up requests. 178 | 179 | ##### request.send(callback) 180 | * parameter 181 | * callback - the function to run when the request is complete. 182 | 183 | -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by toddgeist on 5/4/15. 3 | */ 4 | 5 | 6 | fms = require('fms-js'); 7 | 8 | var options = { 9 | url : '' , 10 | userName : "userName", 11 | password : "password" 12 | }; 13 | 14 | var connection = fms.connection(options); 15 | 16 | var findAllRequest = connection.db('contacts').layout('webContacts').findall(); 17 | findAllRequest.send(function(err, result){ 18 | console.log(result) 19 | }); 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by toddgeist on 5/3/15. 3 | */ 4 | /** 5 | * Created by toddgeist on 5/3/15. 6 | */ 7 | 8 | module.exports = require('./src/filemakerserver'); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fms-js", 3 | "version": "0.2.7", 4 | "description": "FileMaker Server Connection for Node and the browser", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/geistinteractive/fms-js.git" 12 | }, 13 | "keywords": [ 14 | "FileMaker", 15 | "Database" 16 | ], 17 | "author": "Todd Geist (http://www.geisintertactive.com)", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/geistinteractive/fms-js/issues" 21 | }, 22 | "homepage": "https://github.com/geistinteractive/fms-js", 23 | "dependencies": { 24 | "is": "^3.0.1", 25 | "lodash": "^3.8.0", 26 | "superagent": "^1.2.0", 27 | "superagent-xml2jsparser": "^0.1.1" 28 | }, 29 | "devDependencies": { 30 | "chai": "^2.3.0", 31 | "mocha": "^2.2.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/FMSJSError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by toddgeist on 5/3/15. 3 | */ 4 | 5 | 6 | module.exports = exports = FMSJSError; 7 | 8 | /** 9 | * FileMaker Error - based on https://coderwall.com/p/m3-cqw 10 | * @param code 11 | * @param msg 12 | * @constructor 13 | */ 14 | function FMSJSError(message) { 15 | this.message = message 16 | this.name = 'FMSJSError'; 17 | const err = Error(this.message); // http://es5.github.io/#x15.11.1 18 | this.stack = err.stack; 19 | } 20 | 21 | FMSJSError.prototype = Object.create(Error.prototype); 22 | FMSJSError.prototype.constructor = FMSJSError; -------------------------------------------------------------------------------- /src/FileMakerServerError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by toddgeist on 3/31/15. 3 | */ 4 | module.exports = exports = FileMakerServerError; 5 | 6 | /** 7 | * FileMaker Error - based on https://coderwall.com/p/m3-cqw 8 | * @param code 9 | * @param msg 10 | * @constructor 11 | */ 12 | function FileMakerServerError(code, url) { 13 | this.url = url; 14 | this.error = code; 15 | this.message = getMessge(code); 16 | this.name = 'FileMakerServerError'; 17 | console.log('FileMaker Error Code: ',error); 18 | const err = Error(this.message); // http://es5.github.io/#x15.11.1 19 | this.stack = err.stack; 20 | this.isTransient = isTransient(code); 21 | } 22 | 23 | FileMakerServerError.prototype = Object.create(Error.prototype); 24 | FileMakerServerError.prototype.constructor = FileMakerServerError; 25 | 26 | function isTransient(error){ 27 | return (error === '401' || error === '8003' || error === '301') 28 | } 29 | 30 | function getMessge(errorCode){ 31 | var returnString = ""; 32 | 33 | var errNo = parseInt(errorCode); 34 | switch (errNo) 35 | { 36 | case -1: returnString="Unknown error"; break; 37 | case 0: returnString="No error"; break; 38 | case 1: returnString="User canceled action"; break; 39 | case 2: returnString="Memory error"; break; 40 | case 3: returnString="Command is unavailable (for example, wrong operating system, wrong mode, etc.)"; break; 41 | case 4: returnString="Command is unknown"; break; 42 | case 5: returnString="Command is invalid (for example, a Set Field script step does not have a calculation specified)"; break; 43 | case 6: returnString="File is read-only"; break; 44 | case 7: returnString="Running out of memory"; break; 45 | case 8: returnString="Empty result"; break; 46 | case 9: returnString="Insufficient privileges"; break; 47 | case 10: returnString="Requested data is missing"; break; 48 | case 11: returnString="Name is not valid"; break; 49 | case 12: returnString="Name already exists"; break; 50 | case 13: returnString="File or object is in use"; break; 51 | case 14: returnString="Out of range"; break; 52 | case 15: returnString="Can't divide by zero"; break; 53 | case 16: returnString="Operation failed, request retry (for example, a user query)"; break; 54 | case 17: returnString="Attempt to convert foreign character set to UTF-16 failed"; break; 55 | case 18: returnString="Client must provide account information to proceed"; break; 56 | case 19: returnString="String contains characters other than A-Z, a-z, 0-9 (ASCII)"; break; 57 | case 100: returnString="File is missing"; break; 58 | case 101: returnString="Record is missing"; break; 59 | case 102: returnString="Field is missing"; break; 60 | case 103: returnString="Relationship is missing"; break; 61 | case 104: returnString="Script is missing"; break; 62 | case 105: returnString="Layout is missing"; break; 63 | case 106: returnString="Table is missing"; break; 64 | case 107: returnString="Index is missing"; break; 65 | case 108: returnString="Value list is missing"; break; 66 | case 109: returnString="Privilege set is missing"; break; 67 | case 110: returnString="Related tables are missing"; break; 68 | case 111: returnString="Field repetition is invalid"; break; 69 | case 112: returnString="Window is missing"; break; 70 | case 113: returnString="Function is missing"; break; 71 | case 114: returnString="File reference is missing"; break; 72 | case 130: returnString="Files are damaged or missing and must be reinstalled"; break; 73 | case 131: returnString="Language pack files are missing (such as template files)"; break; 74 | case 200: returnString="Record access is denied"; break; 75 | case 201: returnString="Field cannot be modified"; break; 76 | case 202: returnString="Field access is denied"; break; 77 | case 203: returnString="No records in file to print, or password doesn't allow print access"; break; 78 | case 204: returnString="No access to field(s) in sort order"; break; 79 | case 205: returnString="User does not have access privileges to create new records; import will overwrite existing data"; break; 80 | case 206: returnString="User does not have password change privileges, or file is not modifiable"; break; 81 | case 207: returnString="User does not have sufficient privileges to change database schema, or file is not modifiable"; break; 82 | case 208: returnString="Password does not contain enough characters"; break; 83 | case 209: returnString="New password must be different from existing one"; break; 84 | case 210: returnString="User account is inactive"; break; 85 | case 211: returnString="Password has expired"; break; 86 | case 212: returnString="Invalid user account and/or password. Please try again"; break; 87 | case 213: returnString="User account and/or password does not exist"; break; 88 | case 214: returnString="Too many login attempts"; break; 89 | case 215: returnString="Administrator privileges cannot be duplicated"; break; 90 | case 216: returnString="Guest account cannot be duplicated"; break; 91 | case 217: returnString="User does not have sufficient privileges to modify administrator account"; break; 92 | case 300: returnString="File is locked or in use"; break; 93 | case 301: returnString="Record is in use by another user"; break; 94 | case 302: returnString="Table is in use by another user"; break; 95 | case 303: returnString="Database schema is in use by another user"; break; 96 | case 304: returnString="Layout is in use by another user"; break; 97 | case 306: returnString="Record modification ID does not match"; break; 98 | case 400: returnString="Find criteria are empty"; break; 99 | case 401: returnString="No records match the request"; break; 100 | case 402: returnString="Selected field is not a match field for a lookup"; break; 101 | case 403: returnString="Exceeding maximum record limit for trial version of FileMaker Pro"; break; 102 | case 404: returnString="Sort order is invalid"; break; 103 | case 405: returnString="Number of records specified exceeds number of records that can be omitted"; break; 104 | case 406: returnString="Replace/Reserialize criteria are invalid"; break; 105 | case 407: returnString="One or both match fields are missing (invalid relationship)"; break; 106 | case 408: returnString="Specified field has inappropriate data type for this operation"; break; 107 | case 409: returnString="Import order is invalid"; break; 108 | case 410: returnString="Export order is invalid"; break; 109 | case 412: returnString="Wrong version of FileMaker Pro used to recover file"; break; 110 | case 413: returnString="Specified field has inappropriate field type"; break; 111 | case 414: returnString="Layout cannot display the result"; break; 112 | case 415: returnString="Related Record Required"; break; 113 | case 500: returnString="Date value does not meet validation entry options"; break; 114 | case 501: returnString="Time value does not meet validation entry options"; break; 115 | case 502: returnString="Number value does not meet validation entry options"; break; 116 | case 503: returnString="Value in field is not within the range specified in validation entry options"; break; 117 | case 504: returnString="Value in field is not unique as required in validation entry options"; break; 118 | case 505: returnString="Value in field is not an existing value in the database file as required in validation entry options"; break; 119 | case 506: returnString="Value in field is not listed on the value list specified in validation entry option"; break; 120 | case 507: returnString="Value in field failed calculation test of validation entry option"; break; 121 | case 508: returnString="Invalid value entered in Find mode"; break; 122 | case 509: returnString="Field requires a valid value"; break; 123 | case 510: returnString="Related value is empty or unavailable"; break; 124 | case 511: returnString="Value in field exceeds maximum number of allowed characters"; break; 125 | case 600: returnString="Print error has occurred"; break; 126 | case 601: returnString="Combined header and footer exceed one page"; break; 127 | case 602: returnString="Body doesn't fit on a page for current column setup"; break; 128 | case 603: returnString="Print connection lost"; break; 129 | case 700: returnString="File is of the wrong file type for import"; break; 130 | case 706: returnString="EPSF file has no preview image"; break; 131 | case 707: returnString="Graphic translator cannot be found"; break; 132 | case 708: returnString="Can't import the file or need color monitor support to import file"; break; 133 | case 709: returnString="QuickTime movie import failed"; break; 134 | case 710: returnString="Unable to update QuickTime file reference because the database file is read-only"; break; 135 | case 711: returnString="Import translator cannot be found"; break; 136 | case 714: returnString="Password privileges do not allow the operation"; break; 137 | case 715: returnString="Specified Excel worksheet or named range is missing"; break; 138 | case 716: returnString="A SQL query using DELETE, INSERT, or UPDATE is not allowed for ODBC import"; break; 139 | case 717: returnString="There is not enough XML/XSL information to proceed with the import or export"; break; 140 | case 718: returnString="Error in parsing XML file (from Xerces)"; break; 141 | case 719: returnString="Error in transforming XML using XSL (from Xalan)"; break; 142 | case 720: returnString="Error when exporting; intended format does not support repeating fields"; break; 143 | case 721: returnString="Unknown error occurred in the parser or the transformer"; break; 144 | case 722: returnString="Cannot import data into a file that has no fields"; break; 145 | case 723: returnString="You do not have permission to add records to or modify records in the target table"; break; 146 | case 724: returnString="You do not have permission to add records to the target table"; break; 147 | case 725: returnString="You do not have permission to modify records in the target table"; break; 148 | case 726: returnString="There are more records in the import file than in the target table. Not all records were imported"; break; 149 | case 727: returnString="There are more records in the target table than in the import file. Not all records were updated"; break; 150 | case 729: returnString="Errors occurred during import. Records could not be imported"; break; 151 | case 730: returnString="Unsupported Excel version. (Convert file to Excel 7.0 (Excel 95), Excel 97, 2000, or XP format and try again)"; break; 152 | case 731: returnString="The file you are importing from contains no data"; break; 153 | case 732: returnString="This file cannot be inserted because it contains other files"; break; 154 | case 733: returnString="A table cannot be imported into itself"; break; 155 | case 734: returnString="This file type cannot be displayed as a picture"; break; 156 | case 735: returnString="This file type cannot be displayed as a picture. It will be inserted and displayed as a file"; break; 157 | case 800: returnString="Unable to create file on disk"; break; 158 | case 801: returnString="Unable to create temporary file on System disk"; break; 159 | case 802: returnString="Unable to open file"; break; 160 | case 803: returnString="File is single user or host cannot be found"; break; 161 | case 804: returnString="File cannot be opened as read-only in its current state"; break; 162 | case 805: returnString="File is damaged; use Recover command"; break; 163 | case 806: returnString="File cannot be opened with this version of FileMaker Pro"; break; 164 | case 807: returnString="File is not a FileMaker Pro file or is severely damaged"; break; 165 | case 808: returnString="Cannot open file because access privileges are damaged"; break; 166 | case 809: returnString="Disk/volume is full"; break; 167 | case 810: returnString="Disk/volume is locked"; break; 168 | case 811: returnString="Temporary file cannot be opened as FileMaker Pro file"; break; 169 | case 813: returnString="Record Synchronization error on network"; break; 170 | case 814: returnString="File(s) cannot be opened because maximum number is open"; break; 171 | case 815: returnString="Couldn't open lookup file"; break; 172 | case 816: returnString="Unable to convert file"; break; 173 | case 817: returnString="Unable to open file because it does not belong to this solution"; break; 174 | case 819: returnString="Cannot save a local copy of a remote file"; break; 175 | case 820: returnString="File is in the process of being closed"; break; 176 | case 821: returnString="Host forced a disconnect"; break; 177 | case 822: returnString="FMI files not found; reinstall missing files"; break; 178 | case 823: returnString="Cannot set file to single-user, guests are connected"; break; 179 | case 824: returnString="File is damaged or not a FileMaker file"; break; 180 | case 900: returnString="General spelling engine error"; break; 181 | case 901: returnString="Main spelling dictionary not installed"; break; 182 | case 902: returnString="Could not launch the Help system"; break; 183 | case 903: returnString="Command cannot be used in a shared file"; break; 184 | case 904: returnString="Command can only be used in a file hosted under FileMaker Server"; break; 185 | case 905: returnString="No active field selected; command can only be used if there is an active field"; break; 186 | case 920: returnString="Can't initialize the spelling engine"; break; 187 | case 921: returnString="User dictionary cannot be loaded for editing"; break; 188 | case 922: returnString="User dictionary cannot be found"; break; 189 | case 923: returnString="User dictionary is read-only"; break; 190 | case 951: returnString="An unexpected error occurred (returned only by web-published databases)"; break; 191 | case 954: returnString="Unsupported XML grammar (returned only by web-published databases)"; break; 192 | case 955: returnString="No database name (returned only by web-published databases)"; break; 193 | case 956: returnString="Maximum number of database sessions exceeded (returned only by web-published databases)"; break; 194 | case 957: returnString="Conflicting commands (returned only by web-published databases)"; break; 195 | case 958: returnString="Parameter missing (returned only by web-published databases)"; break; 196 | case 971: returnString="The user name is invalid."; break; 197 | case 972: returnString="The password is invalid."; break; 198 | case 973: returnString="The database is invalid."; break; 199 | case 974: returnString="Permission Denied."; break; 200 | case 975: returnString="The field has restricted access."; break; 201 | case 976: returnString="Security is disabled."; break; 202 | case 977: returnString="Invalid client IP address."; break; 203 | case 978: returnString="The number of allowed guests has been exceeded"; break; 204 | case 1200: returnString="Generic calculation error"; break; 205 | case 1201: returnString="Too few parameters in the function"; break; 206 | case 1202: returnString="Too many parameters in the function"; break; 207 | case 1203: returnString="Unexpected end of calculation"; break; 208 | case 1204: returnString="Number, text constant, field name or '(' expected"; break; 209 | case 1205: returnString="Comment is not terminated with '*/'"; break; 210 | case 1206: returnString="Text constant must end with a quotation mark"; break; 211 | case 1207: returnString="Unbalanced parenthesis"; break; 212 | case 1208: returnString="Operator missing, function not found or '(' not expected"; break; 213 | case 1209: returnString="Name (such as field name or layout name) is missing"; break; 214 | case 1210: returnString="Plug-in function has already been registered"; break; 215 | case 1211: returnString="List usage is not allowed in this function"; break; 216 | case 1212: returnString="An operator (for example, +, -, *) is expected here"; break; 217 | case 1213: returnString="This variable has already been defined in the Let function"; break; 218 | case 1214: returnString="AVERAGE, COUNT, EXTEND, GETREPETITION, MAX, MIN, NPV, STDEV, SUM and GETSUMMARY: expression found where a field alone is needed"; break; 219 | case 1215: returnString="This parameter is an invalid Get function parameter"; break; 220 | case 1216: returnString="Only Summary fields allowed as first argument in GETSUMMARY"; break; 221 | case 1217: returnString="Break field is invalid"; break; 222 | case 1218: returnString="Cannot evaluate the number"; break; 223 | case 1219: returnString="A field cannot be used in its own formula"; break; 224 | case 1220: returnString="Field type must be normal or calculated"; break; 225 | case 1221: returnString="Data type must be number, date, time, or timestamp"; break; 226 | case 1222: returnString="Calculation cannot be stored"; break; 227 | case 1223: returnString="The function referred to does not exist"; break; 228 | case 1400: returnString="ODBC driver initialization failed; make sure the ODBC drivers are properly installed"; break; 229 | case 1401: returnString="Failed to allocate environment (ODBC)"; break; 230 | case 1402: returnString="Failed to free environment (ODBC)"; break; 231 | case 1403: returnString="Failed to disconnect (ODBC)"; break; 232 | case 1404: returnString="Failed to allocate connection (ODBC)"; break; 233 | case 1405: returnString="Failed to free connection (ODBC)"; break; 234 | case 1406: returnString="Failed check for SQL API (ODBC)"; break; 235 | case 1407: returnString="Failed to allocate statement (ODBC)"; break; 236 | case 8003: returnString="Record is locked. Similar to 301"; break; 237 | } 238 | return returnString; 239 | } 240 | -------------------------------------------------------------------------------- /src/createLayout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by toddgeist on 5/3/15. 3 | */ 4 | 5 | var creatRequest = require('./createRequest'); 6 | var _ = require('lodash'); 7 | 8 | /** 9 | * creates a layout object 10 | * 11 | * The layout object can be used to create requests 12 | * with a set of factory functions. Those requests 13 | * can be sent by calling .send( callback ) 14 | * 15 | * @param request 16 | * @param dbname 17 | * @param name 18 | * @returns {{query: Function, findany: Function, find: Function, create: Function, new: Function, edit: Function, remove: Function, delete: Function}} 19 | */ 20 | var createLayout = function(postFactory, dbname, name){ 21 | 22 | var postFactory = postFactory; 23 | var dbname = dbname; 24 | var name = name 25 | 26 | var baseOptions = function () { 27 | return { 28 | "-db" : dbname, 29 | "-lay" : name 30 | } 31 | } 32 | 33 | /** 34 | * creates a generic query request. you will have to provide all options 35 | * @param options 36 | * @returns {*} 37 | */ 38 | var query = function (options) { 39 | var requestOptions = baseOptions() 40 | requestOptions = _.extend(requestOptions, options) 41 | 42 | return creatRequest(postFactory() ,requestOptions) 43 | } 44 | 45 | /** 46 | * creates a findany request 47 | * @param options 48 | * @returns {*} 49 | */ 50 | var findany = function (options) { 51 | var requestOptions = baseOptions() 52 | requestOptions = _.extend(requestOptions, options) 53 | requestOptions['-findany'] = '' 54 | return creatRequest(postFactory() , requestOptions ) 55 | } 56 | 57 | /** 58 | * creates a findall request 59 | * @param options 60 | * @returns {*} 61 | */ 62 | var findall = function (options) { 63 | var requestOptions = baseOptions() 64 | requestOptions = _.extend(requestOptions, options) 65 | requestOptions['-findall'] = '' 66 | return creatRequest(postFactory() , requestOptions) 67 | } 68 | 69 | /** 70 | * creates a find request 71 | * @param criteria 72 | * @param options 73 | * @returns {*} 74 | */ 75 | var find = function (criteria, options) { 76 | var requestOptions = baseOptions() 77 | requestOptions = _.extend(requestOptions, criteria) 78 | requestOptions = _.extend(requestOptions, options) 79 | requestOptions['-find'] = '' 80 | return creatRequest(postFactory(), requestOptions ) 81 | } 82 | 83 | /** 84 | * creates a create record request 85 | * @param data 86 | * @param options 87 | * @returns {*} 88 | */ 89 | var create = function(data, options){ 90 | var requestOptions = baseOptions() 91 | requestOptions = _.extend(requestOptions, data) 92 | requestOptions = _.extend(requestOptions, options) 93 | requestOptions['-new'] = ''; 94 | return creatRequest(postFactory(), requestOptions ) 95 | } 96 | 97 | /** 98 | * creates an edit request 99 | * @param data 100 | * @param options 101 | * @returns {*} 102 | */ 103 | var edit = function(data, options){ 104 | var requestOptions = baseOptions() 105 | requestOptions = _.extend(requestOptions, data) 106 | requestOptions = _.extend(requestOptions, options) 107 | requestOptions['-edit'] = ''; 108 | return creatRequest(postFactory(), requestOptions ); 109 | } 110 | 111 | /**git 112 | * creates a delete record request 113 | * @param recordid 114 | * @param options 115 | * @returns {*} 116 | */ 117 | var remove = function(recordid, options){ 118 | var requestOptions = baseOptions() 119 | requestOptions = _.extend(requestOptions, options) 120 | requestOptions['-recid'] = recordid; 121 | requestOptions['-delete'] = ''; 122 | return creatRequest(postFactory() , requestOptions ); 123 | } 124 | 125 | return { 126 | query : query, 127 | findany : findany, 128 | findall : findall, 129 | find : find, 130 | create : create, 131 | new : create, 132 | edit : edit, 133 | remove : remove, 134 | delete : remove 135 | 136 | } 137 | 138 | } 139 | 140 | module.exports = createLayout -------------------------------------------------------------------------------- /src/createRequest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by toddgeist on 5/3/15. 3 | */ 4 | 'use strict' 5 | var _ = require('lodash'); 6 | var is = require('is'); 7 | 8 | var FileMakerServerError = require('./FileMakerServerError'); 9 | 10 | 11 | /** 12 | * creates a query object 13 | * @param post 14 | * @param options 15 | * @returns {{send: Function}} 16 | */ 17 | var createQuery = function (post, options) { 18 | 19 | var options = options 20 | var post = post 21 | // .send(options) 22 | .set('Content-Type', 'application/x-www-form-urlencoded'); 23 | 24 | 25 | var send = function(cb){ 26 | post.send(options).end(function (err, response) { 27 | if(err) return cb(err) 28 | 29 | var fmResponse = parseFMResponse(response.body.fmresultset); 30 | if (is.instance (fmResponse, FileMakerServerError)) { 31 | fmResponse.url = post.url 32 | fmResponse.data = post._data 33 | cb ( fmResponse ) 34 | }else{ 35 | cb (null, fmResponse ) 36 | } 37 | }) 38 | }; 39 | 40 | var set = function(name, value){ 41 | options[name]=value 42 | return this 43 | } 44 | 45 | return { 46 | send : send, 47 | set : set 48 | } 49 | }; 50 | 51 | 52 | /** 53 | * handle the response from the request 54 | * @param fmresultset 55 | * @returns {*} 56 | */ 57 | var parseFMResponse = function (fmresultset) { 58 | var error = fmresultset.error[0].$.code 59 | if(error != 0 ){ 60 | var err = new FileMakerServerError(error,''); 61 | //some errors are transient, they should just return empty sets and basic info 62 | if(err.isTransient){ 63 | return { 64 | error : error , 65 | errorMessage : err.message 66 | } 67 | }else{ 68 | return err 69 | } 70 | } 71 | var recordset = fmresultset.resultset[0] 72 | var records = recordset.record 73 | var data 74 | if (records){ 75 | data = records.map(function (record) { 76 | var obj = remapFields(record.field); 77 | 78 | var relatedSets = record.relatedset 79 | if(relatedSets){ 80 | obj.relatedSets = handleRelatedSets(relatedSets) 81 | } 82 | obj.modid= record.$['mod-id']; 83 | obj.recid = record.$['record-id']; 84 | return obj 85 | }) 86 | }else{ 87 | data = []; 88 | } 89 | 90 | 91 | return { 92 | totalRecords : recordset.$.count , 93 | error : error, 94 | fetchSize : recordset.$['fetch-size'], 95 | data : data 96 | } 97 | 98 | }; 99 | 100 | 101 | /** 102 | * change the Object structure into { field : value, field2 , value2 } 103 | * @param fields 104 | * @returns {{}} 105 | */ 106 | var remapFields = function (fields) { 107 | var obj = {}; 108 | if (fields) { 109 | fields.forEach(function (field) { 110 | obj[field.$.name] = field.data[0] 111 | }); 112 | } 113 | return obj 114 | }; 115 | 116 | 117 | /** 118 | * handle all the relatedSets ie Portals 119 | * @param relatedSets 120 | * @returns {Array} 121 | */ 122 | var handleRelatedSets = function(relatedSets){ 123 | var result = []; 124 | relatedSets.forEach(function(relatedSet){ 125 | var obj = { 126 | count: relatedSet.$.count, 127 | table: relatedSet.$.table 128 | } 129 | var records = relatedSet.record 130 | var data 131 | if (records){ 132 | data = records.map(function (record) { 133 | var obj = remapFields(record.field); 134 | obj.modid= record.$['mod-id']; 135 | obj.recid = record.$['record-id']; 136 | return obj 137 | }) 138 | }else{ 139 | data = []; 140 | } 141 | 142 | obj.data = data 143 | result.push(obj) 144 | }) 145 | 146 | return result 147 | 148 | } 149 | 150 | 151 | module.exports = createQuery -------------------------------------------------------------------------------- /src/database.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by toddgeist on 5/3/15. 3 | */ 4 | 5 | var createRequest = require('./createRequest'); 6 | var createLayout = require('./createLayout'); 7 | var FMSJSError = require('./FMSJSError'); 8 | 9 | 10 | /** 11 | * creates a DB object 12 | * @param request 13 | * @param dbname 14 | */ 15 | var db = function (postFactory, name){ 16 | var postFactory = postFactory; 17 | var dbname = name; 18 | 19 | if(dbname === undefined){ 20 | throw new FMSJSError('name is a required parameter') 21 | } 22 | 23 | var createScriptNamesRequest = function () { 24 | var opts = { 25 | "-db" : dbname, 26 | "-scriptnames" : "" 27 | } 28 | return createRequest(postFactory(), opts ) 29 | } 30 | 31 | var createLayoutNamesRequest = function () { 32 | var opts = { 33 | "-db" : dbname, 34 | "-layoutnames" : "" 35 | } 36 | return createRequest(postFactory(), opts ) 37 | } 38 | 39 | var layout = function(name){ 40 | return createLayout(postFactory, dbname, name) 41 | } 42 | 43 | return { 44 | scriptnames : createScriptNamesRequest, 45 | layoutnames : createLayoutNamesRequest, 46 | layout : layout 47 | } 48 | 49 | 50 | } 51 | 52 | module.exports = db -------------------------------------------------------------------------------- /src/dbNamesRequest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by toddgeist on 5/3/15. 3 | */ 4 | 'use strict' 5 | var createQuery = require('./createRequest'); 6 | 7 | /** 8 | * creates a query request with no options 9 | * @param request 10 | * @returns {*} 11 | */ 12 | module.exports = function (request) { 13 | 14 | return createQuery(request, {'-dbnames' : '' } ); 15 | 16 | } -------------------------------------------------------------------------------- /src/filemakerserver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by toddgeist on 3/30/15. 3 | */ 4 | 5 | 'use strict' 6 | 7 | var request = require('superagent'); 8 | var xml2jsParser = require('superagent-xml2jsparser'); 9 | var dbNamesRequest = require('./dbNamesRequest'); 10 | var database = require('./database'); 11 | 12 | var END_POINT = '/fmi/xml/fmresultset.xml' 13 | 14 | 15 | 16 | /** 17 | * creates a FMS Connection 18 | * @param options 19 | * @returns {{post: Request, query: Function}} 20 | */ 21 | var connection = function (options) { 22 | 23 | var url = options.url; 24 | var protocol = options.protocol || 'http'; 25 | var userName = options.userName; 26 | var password = options.password; 27 | 28 | 29 | var createPostRequest = function () { 30 | var post = request 31 | .post(protocol + '://' + url + END_POINT) 32 | .auth(userName, password) 33 | .accept('xml') 34 | .parse(xml2jsParser); 35 | return post 36 | }; 37 | 38 | var db = function (name) { 39 | return database(createPostRequest, name) 40 | } 41 | 42 | return { 43 | dbnames : function(){return dbNamesRequest( createPostRequest() )}, 44 | db : db 45 | } 46 | 47 | } 48 | 49 | module.exports = { 50 | connection : connection 51 | } -------------------------------------------------------------------------------- /src/old.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by toddgeist on 3/30/15. 3 | */ 4 | 5 | 'use strict' 6 | var request = require('superagent'); 7 | var _ = require('lodash'); 8 | var is = require('is'); 9 | var xml2jsParser = require('superagent-xml2jsparser'); 10 | var FileMakerError = require('./FileMakerError'); 11 | var END_POINT = '/fmi/xml/fmresultset.xml' 12 | 13 | 14 | 15 | /** 16 | * creates a FMS Connection 17 | * @param options 18 | * @returns {{post: Request, query: Function}} 19 | */ 20 | var connection = function (options) { 21 | 22 | var url = options.url; 23 | var protocol = options.protocol || 'http'; 24 | var userName = options.userName; 25 | var password = options.password; 26 | 27 | /** 28 | * creates a Post Request 29 | * @returns {Request} 30 | */ 31 | var createPostRequest = function () { 32 | var post = request 33 | .post(protocol + '://' + url + END_POINT) 34 | .auth(userName, password) 35 | .accept('xml') 36 | .parse(xml2jsParser); 37 | return post 38 | }; 39 | 40 | /** 41 | * returns a raw query object. use ".send" to send it. 42 | * @param options 43 | * @returns {{send}|{send: Function}} 44 | */ 45 | var query = function query(options){ 46 | return createQuery(createPostRequest(), options) 47 | }; 48 | 49 | 50 | /** 51 | * returns a query object that is fixed to the given DB and Layout 52 | * @param db 53 | * @param layout 54 | * @returns {{create: Function}} 55 | */ 56 | var layout = function (db, layout) { 57 | var db = db ; 58 | var layout = layout 59 | 60 | /** 61 | * returns a "Create" request. use .send(cb) to send it to the server 62 | * @param data 63 | * @returns {{send}|{send: Function}} 64 | */ 65 | var newRec = function (data) { 66 | data['-db'] = db; 67 | data['-lay'] = layout; 68 | data['-new'] = '' 69 | return createQuery(createPostRequest(), data) 70 | }; 71 | 72 | var edit = function (data) { 73 | data['-db'] = db; 74 | data['-lay'] = layout; 75 | data['-edit'] = '' 76 | return createQuery(createPostRequest(), data) 77 | }; 78 | 79 | var findAll = function () { 80 | var opts = {} 81 | opts['-db'] = db; 82 | opts['-lay'] = layout; 83 | opts['-findall'] = '' 84 | return createQuery(createPostRequest(), opts) 85 | } 86 | 87 | var find = function (findObject) { 88 | var opts = findObject 89 | opts['-db'] = db; 90 | opts['-lay'] = layout; 91 | opts['-find'] = '' 92 | return createQuery(createPostRequest(), opts) 93 | } 94 | 95 | return { 96 | create : newRec, 97 | new : newRec, 98 | findAll : findAll, 99 | find : find, 100 | edit : edit 101 | } 102 | 103 | }; 104 | 105 | var version = function(){ 106 | return createQuery(createPostRequest(), {}) 107 | }; 108 | 109 | return { 110 | version : version, 111 | query : query, 112 | layout : layout 113 | } 114 | 115 | }; 116 | 117 | /** 118 | * creates a query object 119 | * @param post 120 | * @param options 121 | * @returns {{send: Function}} 122 | */ 123 | var createQuery = function (post, options) { 124 | 125 | var options = options 126 | var post = post 127 | .send(options) 128 | .set('Content-Type', 'application/x-www-form-urlencoded'); 129 | 130 | 131 | var send = function(cb){ 132 | post.end(function (err, response) { 133 | if(err) return cb(err) 134 | 135 | var fmResponse = parseFMResponse(response.body.fmresultset); 136 | if (is.instance (fmResponse, FileMakerError)) { 137 | fmResponse.url = post.req.path 138 | cb ( fmResponse ) 139 | }else{ 140 | cb (null, fmResponse ) 141 | } 142 | }) 143 | }; 144 | 145 | var set = function(name, value){ 146 | options[name]=value 147 | return this 148 | } 149 | 150 | return { 151 | send : send, 152 | set : set 153 | } 154 | }; 155 | 156 | var parseFMResponse = function (fmresultset) { 157 | var error = fmresultset.error[0].$.code 158 | if(error != 0 && error != 401){ 159 | return new FileMakerError(error,'') 160 | } 161 | var recordset = fmresultset.resultset[0] 162 | var records = recordset.record 163 | var data 164 | if (records){ 165 | data = records.map(function (record) { 166 | var obj = remapFields(record.field); 167 | 168 | var relatedSets = record.relatedset 169 | if(relatedSets){ 170 | obj.relatedSets = handleRelatedSets(relatedSets) 171 | } 172 | obj.mod_id= record.$['mod-id']; 173 | obj.record_id = record.$['record-id']; 174 | return obj 175 | }) 176 | }else{ 177 | data = []; 178 | } 179 | 180 | 181 | return { 182 | totalRecords : recordset.$.count , 183 | error : error, 184 | fetchSize : recordset.$['fetch-size'], 185 | data : data 186 | } 187 | 188 | }; 189 | 190 | var remapFields = function (fields) { 191 | var obj = {}; 192 | fields.forEach(function (field) { 193 | obj[field.$.name] = field.data[0] 194 | }); 195 | return obj 196 | }; 197 | 198 | var handleRelatedSets = function(relatedSets){ 199 | var result = []; 200 | relatedSets.forEach(function(relatedSet){ 201 | var obj = { 202 | count: relatedSet.$.count, 203 | table: relatedSet.$.table 204 | } 205 | var records = relatedSet.record 206 | var data 207 | if (records){ 208 | data = records.map(function (record) { 209 | var obj = remapFields(record.field); 210 | obj.mod_id= record.$['mod-id']; 211 | obj.record_id = record.$['record-id']; 212 | return obj 213 | }) 214 | }else{ 215 | data = []; 216 | } 217 | 218 | obj.data = data 219 | result.push(obj) 220 | }) 221 | 222 | return result 223 | 224 | } 225 | 226 | module.exports = { 227 | connection : connection 228 | }; 229 | -------------------------------------------------------------------------------- /test/ContactsTest.fmp12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proofgeist/fms-js/93e2b80052f638ea2165e087ba640cd5a398d97d/test/ContactsTest.fmp12 -------------------------------------------------------------------------------- /test/bootstrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by toddgeist on 5/3/15. 3 | */ 4 | 5 | var chai = require('chai'); 6 | var should = chai.should(); 7 | 8 | var fms = require('../index'); 9 | var config = require('./config') 10 | 11 | 12 | global.connection = fms.connection(config); 13 | 14 | global.test = { 15 | dbname : 'ContactsTest', 16 | layoutname : 'Contacts', 17 | findRecordID : '0AD880FE-9B4A-A93D-4FF1-81130554CA52' 18 | }; -------------------------------------------------------------------------------- /test/config-default.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by toddgeist on 5/3/15. 3 | */ 4 | 5 | 6 | /* 7 | make a copy of this file 8 | name it config.js 9 | and enter the required values below 10 | */ 11 | 12 | module.exports = { 13 | // enter the url of the server. ie 202.202.2.12 or test.example.com 14 | url : '', 15 | 16 | //user name 17 | userName : "admin" , 18 | 19 | //password 20 | password : "" 21 | }; -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --timeout 5000 -------------------------------------------------------------------------------- /test/unit/fmsTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by toddgeist on 5/3/15. 3 | */ 4 | 5 | 6 | /* 7 | "connection" is a global created in bootstrap.js 8 | */ 9 | 10 | 11 | describe('FMS', function () { 12 | 13 | describe('#dbnames', function () { 14 | it('should return a object with a prop an array prop "data"', function (done) { 15 | 16 | connection.dbnames().send(function (err, databases) { 17 | 18 | databases 19 | .should.be.an('object') 20 | .with.a.property('data') 21 | .that.is.an('array'); 22 | 23 | done() 24 | }) 25 | }) 26 | }); 27 | 28 | describe( 'db' , function(){ 29 | it('should return an object if given a name' , function ( ){ 30 | connection.db(test.dbname).should.be.an('object') 31 | }) 32 | }); 33 | 34 | describe('#layoutnames', function () { 35 | it.only('should return a object with a prop an array prop "data"', function (done) { 36 | 37 | connection 38 | .db(test.dbname) 39 | .layoutnames() 40 | .send(function (err, layouts) { 41 | 42 | layouts 43 | .should.be.an('object') 44 | .with.a.property('data') 45 | .that.is.an('array'); 46 | 47 | done() 48 | }) 49 | }) 50 | }); 51 | 52 | describe('#scriptnames', function () { 53 | it('should return a object with a prop an array prop "data"', function (done) { 54 | 55 | var db =connection 56 | .db(test.dbname) 57 | 58 | db.scriptnames() 59 | .send(function (err, scripts) { 60 | 61 | scripts 62 | .should.be.an('object') 63 | .with.a.property('data') 64 | .that.is.an('array'); 65 | 66 | done() 67 | }) 68 | }) 69 | }) 70 | 71 | 72 | }) -------------------------------------------------------------------------------- /test/unit/layoutTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by toddgeist on 5/3/15. 3 | * 4 | * connection is a global connection object setup in bootstrap 5 | * test is an object declared in bootstrap 6 | */ 7 | 8 | 9 | // create a layout for our tests 10 | var layout = connection.db(test.dbname).layout(test.layoutname); 11 | 12 | 13 | describe( 'layout' , function(){ 14 | describe( '#query().send()' , function(){ 15 | it('should return a prop "data" that is an array' , function ( done ){ 16 | var request = layout.query({'-findall': ''}).set('-max', 10); 17 | 18 | request.send(function (err, result) { 19 | result.should.have.a.property('data') 20 | .that.is.an('array'); 21 | 22 | done() 23 | }) 24 | 25 | 26 | }) 27 | }); 28 | 29 | 30 | describe( '#find().send()' , function(){ 31 | it('should return the correct record' , function ( done ){ 32 | var request = layout.find({'id': test.findRecordID}) 33 | request.send(function (err, result) { 34 | result.should.have.a.property('data') 35 | .that.is.an('array'); 36 | result.data[0].should.have.a.property('id') 37 | .that.equals(test.findRecordID); 38 | done() 39 | }) 40 | 41 | }) 42 | }) 43 | 44 | 45 | }); --------------------------------------------------------------------------------