diff --git a/README.md b/README.md index 3498004..20afa13 100644 --- a/README.md +++ b/README.md @@ -220,18 +220,19 @@ connect('nedb://memory'); After connecting to a datastore you can import and create clients. A client is created using the create method on the Filemaker class. The FileMaker class accepts an object with the following properties: -| Property | Type | Description | -| ------------- | :------------------: | :---------------------------------------------------------------------------------: | -| database | String | The FileMaker database to connect to | -| server | String | The FileMaker server to use as the host. **Note:** Must be an http or https Domain. | -| user | String | The FileMaker user account to be used when authenticating into the Data API | -| password | String | The FileMaker user account's password. | -| [name] | String | A name for the client. | -| [usage] | Boolean | Track Data API usage for this client. **Note:** Default is `true` | -| [timeout] | Number | The default timeout time for requests **Note:** Default is 0, (no timeout) | -| [concurrency] | Number | The number of concurrent requests that will be made to FileMaker | -| [proxy] | Object | settings for a proxy server | -| [agent] | Object | settings for a custom request agent | +| Property | Type | Description | +| ----------------------------- | :------------------: | :---------------------------------------------------------------------------------: | +| database | String | The FileMaker database to connect to | +| server | String | The FileMaker server to use as the host. **Note:** Must be an http or https Domain. | +| user | String | The FileMaker user account to be used when authenticating into the Data API | +| password | String | The FileMaker user account's password. | +| [name] | String | A name for the client. | +| [usage] | Boolean | Track Data API usage for this client. **Note:** Default is `true` | +| [timeout] | Number | The default timeout time for requests **Note:** Default is 0, (no timeout) | +| [concurrency] | Number | The number of concurrent requests that will be made to FileMaker | +| [proxy] | Object | settings for a proxy server | +| [agent] | Object | settings for a custom request agent | +| [convertLongNumbersToStrings] | Boolean | Converts long numbers like Get(UUIDNumber) and Random() to strings in responses | :warning: You should only use the agent parameter when absolutely necessary. The Data API was designed to be used on https. Deviating from the intended use should be done with caution. diff --git a/package-lock.json b/package-lock.json index c4d8f0a..27d02a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "axios-cookiejar-support": "^1.0.0", "form-data": "^3.0.0", "into-stream": "^5.1.1", + "json-bigint": "^1.0.0", "lodash": "^4.17.15", "marpat": "^3.0.5", "mime-types": "^2.1.26", @@ -1571,6 +1572,14 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/bignumber.js": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", + "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -5140,6 +5149,14 @@ "node": ">=4" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -11033,6 +11050,11 @@ "tweetnacl": "^0.14.3" } }, + "bignumber.js": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", + "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==" + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -13734,6 +13756,14 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "requires": { + "bignumber.js": "^9.0.0" + } + }, "json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", diff --git a/package.json b/package.json index bfe779e..fbec5ac 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "axios-cookiejar-support": "^1.0.0", "form-data": "^3.0.0", "into-stream": "^5.1.1", + "json-bigint": "^1.0.0", "lodash": "^4.17.15", "marpat": "^3.0.5", "mime-types": "^2.1.26", diff --git a/src/models/agent.model.js b/src/models/agent.model.js index f69efe4..c2304a3 100644 --- a/src/models/agent.model.js +++ b/src/models/agent.model.js @@ -11,6 +11,7 @@ const { Connection } = require('./connection.model'); const axios = require('axios'); const axiosCookieJarSupport = require('axios-cookiejar-support').default; const { omit } = require('../utilities'); +const { parse } = require('../utilities/conversion.utilities'); const instance = axios.create(); @@ -111,6 +112,15 @@ class Agent extends EmbeddedDocument { */ proxy: { type: Object + }, + /** + * return long numbers as strings instead of truncated e notation. + * @member Agent#proxy + * @type Boolean + */ + convertLongNumbersToStrings: { + type: Boolean, + default: () => false } }); } @@ -126,6 +136,8 @@ class Agent extends EmbeddedDocument { preInit({ agent, protocol, timeout, concurrency, connection }) { this.concurrency = concurrency > 0 ? concurrency : 1; + this.convertLongNumbersToStrings = !!this.convertLongNumbersToStrings; + this.connection = Connection.create(connection); if (agent) this.globalize(protocol, agent); } @@ -213,6 +225,8 @@ class Agent extends EmbeddedDocument { const id = uuidv4(); const interceptor = instance.interceptors.request.use( ({ httpAgent, httpsAgent, ...request }) => { + request.transformResponse = data => + parse(data, this.convertLongNumbersToStrings); instance.interceptors.request.eject(interceptor); return new Promise((resolve, reject) => this.push({ @@ -384,7 +398,7 @@ class Agent extends EmbeddedDocument { if (token) { this.connection.deactivate(token, id); } - + this.connection.confirm(); if (error.code) { diff --git a/src/models/client.model.js b/src/models/client.model.js index a30df8c..d169f7d 100644 --- a/src/models/client.model.js +++ b/src/models/client.model.js @@ -80,6 +80,7 @@ class Client extends Document { threshold, usage, proxy, + convertLongNumbersToStrings, ...connection } = data; const protocol = data.server.startsWith('https') ? 'https' : 'http'; @@ -91,6 +92,7 @@ class Client extends Document { threshold, concurrency, protocol, + convertLongNumbersToStrings, connection }); } @@ -387,7 +389,9 @@ class Client extends Document { .then(response => response.data) .then(body => this.data.outgoing(body)) .then(body => this._save(body)) - .then(body => parseScriptResult(body)) + .then(body => + parseScriptResult(body, this.agent.convertLongNumbersToStrings) + ) .then(result => resolve(result)) .catch(error => this._save(reject(error))) ); @@ -450,7 +454,9 @@ class Client extends Document { .then(response => response.data) .then(body => this.data.outgoing(body)) .then(body => this._save(body)) - .then(body => parseScriptResult(body)) + .then(body => + parseScriptResult(body, this.agent.convertLongNumbersToStrings) + ) .then(response => resolve(parameters.merge ? Object.assign(data, response) : response) ) @@ -505,7 +511,9 @@ class Client extends Document { .then(response => response.data) .then(body => this.data.outgoing(body)) .then(body => this._save(body)) - .then(body => parseScriptResult(body)) + .then(body => + parseScriptResult(body, this.agent.convertLongNumbersToStrings) + ) .then(body => resolve( parameters.merge @@ -555,7 +563,9 @@ class Client extends Document { .then(response => response.data) .then(body => this.data.outgoing(body)) .then(body => this._save(body)) - .then(body => parseScriptResult(body)) + .then(body => + parseScriptResult(body, this.agent.convertLongNumbersToStrings) + ) .then(result => resolve(result)) .catch(error => this._save(reject(error))) ); @@ -605,7 +615,9 @@ class Client extends Document { .then(response => response.data) .then(body => this.data.outgoing(body)) .then(body => this._save(body)) - .then(body => parseScriptResult(body)) + .then(body => + parseScriptResult(body, this.agent.convertLongNumbersToStrings) + ) .then(result => resolve(result)) .catch(error => this._save(reject(error))) ); @@ -659,7 +671,9 @@ class Client extends Document { .then(response => response.data) .then(body => this.data.outgoing(body)) .then(body => this._save(body)) - .then(body => parseScriptResult(body)) + .then(body => + parseScriptResult(body, this.agent.convertLongNumbersToStrings) + ) .then(result => resolve(result)) .catch(error => this._save(reject(error))) ); @@ -716,7 +730,9 @@ class Client extends Document { .then(response => response.data) .then(body => this.data.outgoing(body)) .then(body => this._save(body)) - .then(body => parseScriptResult(body)) + .then(body => + parseScriptResult(body, this.agent.convertLongNumbersToStrings) + ) .then(result => resolve(result)) .catch(error => { this._save(); @@ -829,7 +845,9 @@ class Client extends Document { .then(response => response.data) .then(body => this.data.outgoing(body)) .then(body => this._save(body)) - .then(body => parseScriptResult(body)) + .then(body => + parseScriptResult(body, this.agent.convertLongNumbersToStrings) + ) .then(response => Object.assign(response, { recordId: resolvedId })) ) .then(response => resolve(response)) @@ -895,7 +913,12 @@ class Client extends Document { .then(response => response.data) .then(body => this.data.outgoing(body)) .then(body => this._save(body)) - .then(body => pick(parseScriptResult(body), 'scriptResult')) + .then(body => + pick( + parseScriptResult(body, this.agent.convertLongNumbersToStrings), + 'scriptResult' + ) + ) .then(result => resolve(result)) .catch(error => this._save(reject(error))) ); diff --git a/src/utilities/conversion.utilities.js b/src/utilities/conversion.utilities.js index e206eb8..62c7f11 100644 --- a/src/utilities/conversion.utilities.js +++ b/src/utilities/conversion.utilities.js @@ -1,6 +1,7 @@ 'use strict'; const _ = require('lodash'); +const JSONbig = require('json-bigint')({ storeAsString: true }); /** * @class Conversion Utilities @@ -105,9 +106,15 @@ const omit = (data, properties) => * @description The parse function performs a try catch before attempting to parse the value as JSON. If the value is not valid JSON it wil return the value. * @see isJSON * @param {Any} value The value to attempt to parse. + * @param {boolean} [convertLongNumbersToStrings] convert long numbers to strings. * @return {Object|Any} A JSON object or array of objects without the properties passed to it */ -const parse = value => (isJSON(value) ? JSON.parse(value) : value); +const parse = (value, convertLongNumbersToStrings) => + isJSON(value) + ? convertLongNumbersToStrings + ? JSONbig.parse(value) + : JSON.parse(value) + : value; /** * @function pick diff --git a/src/utilities/filemaker.utilities.js b/src/utilities/filemaker.utilities.js index 26266d3..318f272 100644 --- a/src/utilities/filemaker.utilities.js +++ b/src/utilities/filemaker.utilities.js @@ -111,19 +111,20 @@ const sanitizeParameters = (parameters, safeParameters) => ); /** - * @function parseScriptResults + * @function parseScriptResult * @public * @memberof Filemaker Utilities * @description The parseScriptResults function filters the FileMaker DAPI response by testing if a script was triggered * with the request, then either selecting the response, script error, and script result from the * response or selecting just the response. * @param {Object} data The response recieved from the FileMaker DAPI. + * @param {boolean} convertLongNumbersToStrings convert long numbers to strings * @return {Object} A json object containing the selected data from the Data API Response. */ -const parseScriptResult = data => +const parseScriptResult = (data, convertLongNumbersToStrings) => _.mapValues(data.response, (value, property, object) => property.includes('scriptResult') - ? (object[property] = parse(value)) + ? (object[property] = parse(value, convertLongNumbersToStrings)) : value ); diff --git a/test/agent.test.js b/test/agent.test.js index 18d5454..19ac745 100644 --- a/test/agent.test.js +++ b/test/agent.test.js @@ -73,6 +73,7 @@ describe('Agent Configuration Capabilities', () => { '_schema', 'concurrency', 'connection', + 'convertLongNumbersToStrings', 'queue', 'delay', 'pending', @@ -120,6 +121,7 @@ describe('Agent Configuration Capabilities', () => { 'agent', 'connection', 'concurrency', + 'convertLongNumbersToStrings', 'delay', 'pending', 'queue', @@ -150,6 +152,7 @@ describe('Agent Configuration Capabilities', () => { 'agent', 'concurrency', 'connection', + 'convertLongNumbersToStrings', 'delay', 'global', 'pending', @@ -253,6 +256,7 @@ describe('Agent Configuration Capabilities', () => { 'agent', 'concurrency', 'connection', + 'convertLongNumbersToStrings', 'delay', 'global', 'pending', @@ -268,7 +272,7 @@ describe('Agent Configuration Capabilities', () => { it('should use a proxy if one is set', () => { http - .createServer(function(req, res) { + .createServer(function (req, res) { proxy.web(req, res, { target: process.env.SERVER }); @@ -338,6 +342,7 @@ describe('Agent Configuration Capabilities', () => { '_schema', 'protocol', 'connection', + 'convertLongNumbersToStrings', 'global', 'proxy', 'timeout', @@ -369,6 +374,7 @@ describe('Agent Configuration Capabilities', () => { '_schema', 'protocol', 'connection', + 'convertLongNumbersToStrings', 'global', 'proxy', 'timeout', @@ -403,6 +409,88 @@ describe('Agent Configuration Capabilities', () => { .to.equal('ECONNABORTED'); }); + describe('convertLongNumbersToStrings option', () => { + it('should return strings in fieldData if enabled', () => { + const client = Filemaker.create({ + database: process.env.DATABASE, + server: process.env.SERVER, + user: process.env.USERNAME, + password: process.env.PASSWORD, + usage: true, + convertLongNumbersToStrings: true + }); + return expect( + client + .save() + .then(client => client.list(process.env.LAYOUT, { limit: 1 })) + .then(res => res.data[0].fieldData.uuidNumber) + .catch(error => error) + ).to.eventually.be.a('string'); + }); + + it('should return numbers in fieldData by default', () => { + const client = Filemaker.create({ + database: process.env.DATABASE, + server: process.env.SERVER, + user: process.env.USERNAME, + password: process.env.PASSWORD, + usage: true + }); + return expect( + client + .save() + .then(client => client.list(process.env.LAYOUT, { limit: 1 })) + .then(res => res.data[0].fieldData.uuidNumber) + .catch(error => error) + ).to.eventually.be.a('number'); + }); + + it('should return strings in scriptResult if enabled', () => { + const client = Filemaker.create({ + database: process.env.DATABASE, + server: process.env.SERVER, + user: process.env.USERNAME, + password: process.env.PASSWORD, + usage: true, + convertLongNumbersToStrings: true + }); + return expect( + client + .save() + .then(client => + client.list(process.env.LAYOUT, { + limit: 1, + script: 'Long Number JSON Script' + }) + ) + .then(res => res.scriptResult.longNumber) + .catch(error => error) + ).to.eventually.be.a('string'); + }); + + it('should return numbers in scriptResult by default', () => { + const client = Filemaker.create({ + database: process.env.DATABASE, + server: process.env.SERVER, + user: process.env.USERNAME, + password: process.env.PASSWORD, + usage: true + }); + return expect( + client + .save() + .then(client => + client.list(process.env.LAYOUT, { + limit: 1, + script: 'Long Number JSON Script' + }) + ) + .then(res => res.scriptResult.longNumber) + .catch(error => error) + ).to.eventually.be.a('number'); + }); + }); + it('should not try to resolve pending requests that do not have a resolve function', () => { const client = Filemaker.create({ database: process.env.DATABASE, diff --git a/test/utilities.test.js b/test/utilities.test.js index 093238c..428e8c4 100644 --- a/test/utilities.test.js +++ b/test/utilities.test.js @@ -55,6 +55,58 @@ describe('Conversion Utility Capabilities', () => { .to.be.a('object') .and.to.include.keys('name'); }); + describe('Long number handling', () => { + const convertLongNums = true; + it('it should return a string when given a string', () => { + return expect(parse('A String', convertLongNums)).to.be.a('string'); + }); + it('it should return an object when given a stringified object', () => { + return expect( + parse(JSON.stringify({ name: 'Han Solo' }), convertLongNums) + ) + .to.be.a('object') + .and.to.include.keys('name'); + }); + it('it should convert long numbers to strings', () => { + const parsed = parse( + '{"longNum": 123456789012345678901234567890}', + convertLongNums + ); + const longNum = parsed.longNum; + return expect(longNum).to.be.a('string'); + }); + it('it should leave short numbers as numbers', () => { + const parsed = parse('{"shortNum": 123}', convertLongNums); + const shortNum = parsed.shortNum; + return expect(shortNum).to.be.a('number'); + }); + it('it should work with negative numbers', () => { + const parsed = parse( + '{"longNum": -123456789012345678901234567890}', + convertLongNums + ); + const longNum = parsed.longNum; + return expect(longNum).to.eql('-123456789012345678901234567890'); + }); + it('it should work with short negative numbers', () => { + const parsed = parse('{"longNum": -123}', convertLongNums); + const longNum = parsed.longNum; + return expect(longNum).to.eql(-123); + }); + it('it should work with decimals', () => { + const parsed = parse( + '{"longNum": 1.12345678901234567890123456789}', + convertLongNums + ); + const longNum = parsed.longNum; + return expect(longNum).to.eql('1.12345678901234567890123456789'); + }); + it('it should work with short decimals', () => { + const parsed = parse('{"longNum": 1.123}', convertLongNums); + const longNum = parsed.longNum; + return expect(longNum).to.eql(1.123); + }); + }); }); describe('isJSON Utility', () => { it('it should return true for an object', () => {