From da36c281123f0f92869c408ff0642f2c0fea9cc8 Mon Sep 17 00:00:00 2001 From: Airtable Date: Tue, 20 Sep 2022 09:30:48 -0700 Subject: [PATCH] v0.11.5 --- CHANGELOG.md | 4 + build/airtable.browser.js | 87 +++++++++-- package-lock.json | 4 +- package.json | 2 +- src/query.ts | 84 ++++++++--- src/query_params.ts | 11 ++ src/table.ts | 72 +++++++-- test/list.test.js | 217 ++++++++++++++++++++++++++++ test/select.test.js | 77 ++++++++++ test/test_files/airtable.browser.js | 87 +++++++++-- 10 files changed, 591 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26592177..71b0f203 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# v0.11.5 + * Update select() and list() to support to use POST endpoint when GET url length would exceed Airtable's 16k character limit + * Update select() and list() to explicitly choose to use GET or POST endpoint via new 'method' arg + # v0.11.4 * Add support for returnFieldsByFieldId param. diff --git a/build/airtable.browser.js b/build/airtable.browser.js index edddb42a..72b79d33 100644 --- a/build/airtable.browser.js +++ b/build/airtable.browser.js @@ -443,7 +443,7 @@ module.exports = objectToQueryParamString; },{"lodash/isArray":79,"lodash/isNil":85,"lodash/keys":93}],12:[function(require,module,exports){ "use strict"; -module.exports = "0.11.4"; +module.exports = "0.11.5"; },{}],13:[function(require,module,exports){ "use strict"; @@ -467,6 +467,7 @@ var record_1 = __importDefault(require("./record")); var callback_to_promise_1 = __importDefault(require("./callback_to_promise")); var has_1 = __importDefault(require("./has")); var query_params_1 = require("./query_params"); +var object_to_query_param_string_1 = __importDefault(require("./object_to_query_param_string")); /** * Builds a query object. Won't fetch until `firstPage` or * or `eachPage` is called. @@ -554,10 +555,40 @@ function eachPage(pageCallback, done) { if (!isFunction_1.default(done) && done !== void 0) { throw new Error('The second parameter to `eachPage` must be a function or undefined'); } - var path = "/" + this._table._urlEncodedNameOrId(); var params = __assign({}, this._params); + var pathAndParamsAsString = "/" + this._table._urlEncodedNameOrId() + "?" + object_to_query_param_string_1.default(params); + var queryParams = {}; + var requestData = null; + var method; + var path; + if (params.method === 'post' || pathAndParamsAsString.length > query_params_1.URL_CHARACTER_LENGTH_LIMIT) { + // There is a 16kb limit on GET requests. Since the URL makes up nearly all of the request size, we check for any requests that + // that come close to this limit and send it as a POST instead. Additionally, we'll send the request as a post if it is specified + // with the request params + requestData = params; + method = 'post'; + path = "/" + this._table._urlEncodedNameOrId() + "/listRecords"; + var paramNames = Object.keys(params); + for (var _i = 0, paramNames_1 = paramNames; _i < paramNames_1.length; _i++) { + var paramName = paramNames_1[_i]; + if (query_params_1.shouldListRecordsParamBePassedAsParameter(paramName)) { + // timeZone and userLocale is parsed from the GET request separately from the other params. This parsing + // does not occurring within the body parser we use for POST requests, so this will still need to be passed + // via query params + queryParams[paramName] = params[paramName]; + } + else { + requestData[paramName] = params[paramName]; + } + } + } + else { + method = 'get'; + queryParams = params; + path = "/" + this._table._urlEncodedNameOrId(); + } var inner = function () { - _this._table._base.runAction('get', path, params, null, function (err, response, result) { + _this._table._base.runAction(method, path, queryParams, requestData, function (err, response, result) { if (err) { done(err, null); } @@ -603,13 +634,13 @@ function all(done) { } module.exports = Query; -},{"./callback_to_promise":4,"./has":8,"./query_params":14,"./record":15,"lodash/isFunction":83,"lodash/keys":93}],14:[function(require,module,exports){ +},{"./callback_to_promise":4,"./has":8,"./object_to_query_param_string":11,"./query_params":14,"./record":15,"lodash/isFunction":83,"lodash/keys":93}],14:[function(require,module,exports){ "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.paramValidators = void 0; +exports.shouldListRecordsParamBePassedAsParameter = exports.URL_CHARACTER_LENGTH_LIMIT = exports.paramValidators = void 0; var typecheck_1 = __importDefault(require("./typecheck")); var isString_1 = __importDefault(require("lodash/isString")); var isNumber_1 = __importDefault(require("lodash/isNumber")); @@ -634,8 +665,15 @@ exports.paramValidators = { }, 'the value for `cellFormat` should be "json" or "string"'), timeZone: typecheck_1.default(isString_1.default, 'the value for `timeZone` should be a string'), userLocale: typecheck_1.default(isString_1.default, 'the value for `userLocale` should be a string'), + method: typecheck_1.default(function (method) { + return isString_1.default(method) && ['get', 'post'].includes(method); + }, 'the value for `method` should be "get" or "post"'), returnFieldsByFieldId: typecheck_1.default(isBoolean_1.default, 'the value for `returnFieldsByFieldId` should be a boolean'), }; +exports.URL_CHARACTER_LENGTH_LIMIT = 15000; +exports.shouldListRecordsParamBePassedAsParameter = function (paramName) { + return paramName === 'timeZone' || paramName === 'userLocale'; +}; },{"./typecheck":18,"lodash/isBoolean":81,"lodash/isNumber":86,"lodash/isPlainObject":89,"lodash/isString":90}],15:[function(require,module,exports){ "use strict"; @@ -840,6 +878,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) { var isPlainObject_1 = __importDefault(require("lodash/isPlainObject")); var deprecate_1 = __importDefault(require("./deprecate")); var query_1 = __importDefault(require("./query")); +var query_params_1 = require("./query_params"); +var object_to_query_param_string_1 = __importDefault(require("./object_to_query_param_string")); var record_1 = __importDefault(require("./record")); var callback_to_promise_1 = __importDefault(require("./callback_to_promise")); var Table = /** @class */ (function () { @@ -978,15 +1018,42 @@ var Table = /** @class */ (function () { record.destroy(done); } }; - Table.prototype._listRecords = function (limit, offset, opts, done) { + Table.prototype._listRecords = function (pageSize, offset, opts, done) { var _this = this; if (!done) { done = opts; opts = {}; } - var listRecordsParameters = __assign({ limit: limit, - offset: offset }, opts); - this._base.runAction('get', "/" + this._urlEncodedNameOrId() + "/", listRecordsParameters, null, function (err, response, results) { + var pathAndParamsAsString = "/" + this._urlEncodedNameOrId() + "?" + object_to_query_param_string_1.default(opts); + var path; + var listRecordsParameters = {}; + var listRecordsData = null; + var method; + if ((typeof opts !== 'function' && opts.method === 'post') || + pathAndParamsAsString.length > query_params_1.URL_CHARACTER_LENGTH_LIMIT) { + // // There is a 16kb limit on GET requests. Since the URL makes up nearly all of the request size, we check for any requests that + // that come close to this limit and send it as a POST instead. Additionally, we'll send the request as a post if it is specified + // with the request params + path = "/" + this._urlEncodedNameOrId() + "/listRecords"; + listRecordsData = __assign(__assign({}, (pageSize && { pageSize: pageSize })), (offset && { offset: offset })); + method = 'post'; + var paramNames = Object.keys(opts); + for (var _i = 0, paramNames_1 = paramNames; _i < paramNames_1.length; _i++) { + var paramName = paramNames_1[_i]; + if (query_params_1.shouldListRecordsParamBePassedAsParameter(paramName)) { + listRecordsParameters[paramName] = opts[paramName]; + } + else { + listRecordsData[paramName] = opts[paramName]; + } + } + } + else { + method = 'get'; + path = "/" + this._urlEncodedNameOrId() + "/"; + listRecordsParameters = __assign({ limit: pageSize, offset: offset }, opts); + } + this._base.runAction(method, path, listRecordsParameters, listRecordsData, function (err, response, results) { if (err) { done(err); return; @@ -1030,7 +1097,7 @@ var Table = /** @class */ (function () { }()); module.exports = Table; -},{"./callback_to_promise":4,"./deprecate":5,"./query":13,"./record":15,"lodash/isPlainObject":89}],18:[function(require,module,exports){ +},{"./callback_to_promise":4,"./deprecate":5,"./object_to_query_param_string":11,"./query":13,"./query_params":14,"./record":15,"lodash/isPlainObject":89}],18:[function(require,module,exports){ "use strict"; /* eslint-enable @typescript-eslint/no-explicit-any */ function check(fn, error) { diff --git a/package-lock.json b/package-lock.json index d5de151e..6a0b6d79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "airtable", - "version": "0.11.4", + "version": "0.11.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "airtable", - "version": "0.11.3", + "version": "0.11.5", "license": "MIT", "dependencies": { "@types/node": ">=8.0.0 <15", diff --git a/package.json b/package.json index c3b07c0e..086e0c56 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "airtable", - "version": "0.11.4", + "version": "0.11.5", "license": "MIT", "homepage": "https://github.com/airtable/airtable.js", "repository": "git://github.com/airtable/airtable.js.git", diff --git a/src/query.ts b/src/query.ts index 0bc8fbec..7e2bccc3 100644 --- a/src/query.ts +++ b/src/query.ts @@ -4,7 +4,13 @@ import Record from './record'; import callbackToPromise from './callback_to_promise'; import has from './has'; import Table from './table'; -import {paramValidators, QueryParams} from './query_params'; +import { + paramValidators, + QueryParams, + shouldListRecordsParamBePassedAsParameter, + URL_CHARACTER_LENGTH_LIMIT, +} from './query_params'; +import objectToQueryParamString from './object_to_query_param_string'; import {FieldSet} from './field_set'; import {Records} from './records'; @@ -150,31 +156,71 @@ function eachPage( throw new Error('The second parameter to `eachPage` must be a function or undefined'); } - const path = `/${this._table._urlEncodedNameOrId()}`; const params = {...this._params}; + const pathAndParamsAsString = `/${this._table._urlEncodedNameOrId()}?${objectToQueryParamString( + params + )}`; - const inner = () => { - this._table._base.runAction('get', path, params, null, (err, response, result) => { - if (err) { - done(err, null); + let queryParams = {}; + let requestData = null; + let method; + let path; + + if (params.method === 'post' || pathAndParamsAsString.length > URL_CHARACTER_LENGTH_LIMIT) { + // There is a 16kb limit on GET requests. Since the URL makes up nearly all of the request size, we check for any requests that + // that come close to this limit and send it as a POST instead. Additionally, we'll send the request as a post if it is specified + // with the request params + + requestData = params; + method = 'post'; + path = `/${this._table._urlEncodedNameOrId()}/listRecords`; + + const paramNames = Object.keys(params); + + for (const paramName of paramNames) { + if (shouldListRecordsParamBePassedAsParameter(paramName)) { + // timeZone and userLocale is parsed from the GET request separately from the other params. This parsing + // does not occurring within the body parser we use for POST requests, so this will still need to be passed + // via query params + queryParams[paramName] = params[paramName]; } else { - let next; - if (result.offset) { - params.offset = result.offset; - next = inner; + requestData[paramName] = params[paramName]; + } + } + } else { + method = 'get'; + queryParams = params; + path = `/${this._table._urlEncodedNameOrId()}`; + } + + const inner = () => { + this._table._base.runAction( + method, + path, + queryParams, + requestData, + (err, response, result) => { + if (err) { + done(err, null); } else { - next = () => { - done(null); - }; - } + let next; + if (result.offset) { + params.offset = result.offset; + next = inner; + } else { + next = () => { + done(null); + }; + } - const records = result.records.map(recordJson => { - return new Record(this._table, null, recordJson); - }); + const records = result.records.map(recordJson => { + return new Record(this._table, null, recordJson); + }); - pageCallback(records, next); + pageCallback(records, next); + } } - }); + ); }; inner(); diff --git a/src/query_params.ts b/src/query_params.ts index 077fde49..447dd2ec 100644 --- a/src/query_params.ts +++ b/src/query_params.ts @@ -41,12 +41,22 @@ export const paramValidators = { userLocale: check(isString, 'the value for `userLocale` should be a string'), + method: check((method): method is 'json' | 'string' => { + return isString(method) && ['get', 'post'].includes(method); + }, 'the value for `method` should be "get" or "post"'), + returnFieldsByFieldId: check( isBoolean, 'the value for `returnFieldsByFieldId` should be a boolean' ), }; +export const URL_CHARACTER_LENGTH_LIMIT = 15000; + +export const shouldListRecordsParamBePassedAsParameter = (paramName: string): boolean => { + return paramName === 'timeZone' || paramName === 'userLocale'; +}; + export interface SortParameter { field: keyof TFields; direction?: 'asc' | 'desc'; @@ -63,5 +73,6 @@ export interface QueryParams { cellFormat?: 'json' | 'string'; timeZone?: string; userLocale?: string; + method?: string; returnFieldsByFieldId?: boolean; } diff --git a/src/table.ts b/src/table.ts index fa4dc1ea..8660505c 100644 --- a/src/table.ts +++ b/src/table.ts @@ -1,7 +1,12 @@ import isPlainObject from 'lodash/isPlainObject'; import deprecate from './deprecate'; import Query from './query'; -import {QueryParams} from './query_params'; +import { + QueryParams, + shouldListRecordsParamBePassedAsParameter, + URL_CHARACTER_LENGTH_LIMIT, +} from './query_params'; +import objectToQueryParamString from './object_to_query_param_string'; import Record from './record'; import callbackToPromise from './callback_to_promise'; import Base from './base'; @@ -16,7 +21,10 @@ type TableError = any; type CreateRecord = Pick>, 'fields'>; type CreateRecords = string[] | Partial[] | CreateRecord[]; -type OptionalParameters = {typecast?: boolean}; +type OptionalParameters = { + typecast?: boolean; + method?: 'get' | 'post'; +}; type RecordCollectionCallback = ( error: TableError, @@ -92,7 +100,6 @@ interface TableDestroyRecords { (recordIds: string[]): Promise>; (recordIds: string[], done: RecordCollectionCallback): void; } - class Table { readonly _base: Base; @@ -358,7 +365,7 @@ class Table { } _listRecords( - limit: number, + pageSize: number, offset: number, opts: OptionalParameters | RecordListCallback, done?: RecordListCallback @@ -367,17 +374,58 @@ class Table { done = opts as RecordListCallback; opts = {}; } - const listRecordsParameters = { - limit, - offset, - ...opts, - }; + + const pathAndParamsAsString = `/${this._urlEncodedNameOrId()}?${objectToQueryParamString( + opts + )}`; + + let path; + let listRecordsParameters = {}; + let listRecordsData = null; + let method; + + if ( + (typeof opts !== 'function' && opts.method === 'post') || + pathAndParamsAsString.length > URL_CHARACTER_LENGTH_LIMIT + ) { + // // There is a 16kb limit on GET requests. Since the URL makes up nearly all of the request size, we check for any requests that + // that come close to this limit and send it as a POST instead. Additionally, we'll send the request as a post if it is specified + // with the request params + + path = `/${this._urlEncodedNameOrId()}/listRecords`; + listRecordsData = { + // limit is deprecated and the GET request parser in hyperbase automatically + // replaces this but the body parser used for POST requests does not + ...(pageSize && {pageSize}), + // The request parser will error if offset is included and is undefined + ...(offset && {offset}), + }; + method = 'post'; + + const paramNames = Object.keys(opts); + + for (const paramName of paramNames) { + if (shouldListRecordsParamBePassedAsParameter(paramName)) { + listRecordsParameters[paramName] = opts[paramName]; + } else { + listRecordsData[paramName] = opts[paramName]; + } + } + } else { + method = 'get'; + path = `/${this._urlEncodedNameOrId()}/`; + listRecordsParameters = { + limit: pageSize, + offset, + ...opts, + }; + } this._base.runAction( - 'get', - `/${this._urlEncodedNameOrId()}/`, + method, + path, listRecordsParameters, - null, + listRecordsData, (err, response, results) => { if (err) { done(err); diff --git a/test/list.test.js b/test/list.test.js index 2c9e5746..91a2e3fa 100644 --- a/test/list.test.js +++ b/test/list.test.js @@ -280,4 +280,221 @@ describe('list records', function() { } ); }); + + it('should support passing timezone and userLocale as params', function(done) { + testExpressApp.set('handler override', function(req, res) { + expect(req.method).toBe('GET'); + expect(req.url).toBe( + '/v0/app123/Table/?limit=50&offset=offset000&timeZone=America%2FLos_Angeles&userLocale=en-US' + ); + res.json({ + records: [ + { + id: 'recordA', + fields: {Name: 'Rebecca'}, + createdTime: '2020-04-20T16:20:00.000Z', + }, + ], + offset: 'offsetABC', + }); + }); + + return airtable + .base('app123') + .table('Table') + .list(50, 'offset000', {timeZone: 'America/Los_Angeles', userLocale: 'en-US'}, function( + err, + records, + offset + ) { + expect(err).toBeNull(); + expect(records.length).toBe(1); + expect(records[0].getId()).toBe('recordA'); + expect(records[0].get('Name')).toBe('Rebecca'); + expect(offset).toBe('offsetABC'); + done(); + }); + }); + + it('uses POST listRows endpoint when params exceed 15k characters', function(done) { + // Mock formula that is 15000 characters in length + const longFormula = [...Array(657)] + .map((_, i) => { + return `NOT({Name} = '${i}') & `; + }) + .join(); + + expect(longFormula.length).toBe(15000); + + testExpressApp.set('handler override', function(req, res) { + expect(req.method).toBe('POST'); + // Request url should not include and query params and should + // use /listRecords endpoint with request body instead + expect(req.url).toBe( + '/v0/app123/Table/listRecords?timeZone=America%2FLos_Angeles&userLocale=en-US' + ); + expect(req.body.filterByFormula).toBe(longFormula); + res.json({ + records: [ + { + id: 'recordA', + fields: {Name: 'Rebecca'}, + createdTime: '2020-04-20T16:20:00.000Z', + }, + ], + offset: 'offsetABC', + }); + }); + + return airtable + .base('app123') + .table('Table') + .list( + 50, + 'offset000', + { + filterByFormula: longFormula, + timeZone: 'America/Los_Angeles', + userLocale: 'en-US', + }, + function(err, records, offset) { + expect(err).toBeNull(); + expect(records.length).toBe(1); + expect(records[0].getId()).toBe('recordA'); + expect(records[0].get('Name')).toBe('Rebecca'); + expect(offset).toBe('offsetABC'); + done(); + } + ); + }); + + it('uses POST listRows endpoint when "post" is specified for method', function(done) { + testExpressApp.set('handler override', function(req, res) { + expect(req.method).toBe('POST'); + // Request url should not include and query params and should + // use /listRecords endpoint with request body instead + expect(req.url).toBe( + '/v0/app123/Table/listRecords?timeZone=America%2FLos_Angeles&userLocale=en-US' + ); + res.json({ + records: [ + { + id: 'recordA', + fields: {Name: 'Rebecca'}, + createdTime: '2020-04-20T16:20:00.000Z', + }, + ], + offset: 'offsetABC', + }); + }); + + return airtable + .base('app123') + .table('Table') + .list( + 50, + 'offset000', + { + method: 'post', + timeZone: 'America/Los_Angeles', + userLocale: 'en-US', + }, + function(err, records, offset) { + expect(err).toBeNull(); + expect(records.length).toBe(1); + expect(records[0].getId()).toBe('recordA'); + expect(records[0].get('Name')).toBe('Rebecca'); + expect(offset).toBe('offsetABC'); + done(); + } + ); + }); + + it('should support passing pageSize and offset with POST listRows endpoint', function(done) { + testExpressApp.set('handler override', function(req, res) { + expect(req.method).toBe('POST'); + // Request url should not include and query params and should + // use /listRecords endpoint with request body instead + expect(req.url).toBe( + '/v0/app123/Table/listRecords?timeZone=America%2FLos_Angeles&userLocale=en-US' + ); + expect(req.body.pageSize).toBe(1); + expect(req.body.offset).toBe('offset000'); + res.json({ + records: [ + { + id: 'recordA', + fields: {Name: 'Rebecca'}, + createdTime: '2020-04-20T16:20:00.000Z', + }, + ], + offset: 'offsetABC', + }); + }); + + return airtable + .base('app123') + .table('Table') + .list( + 1, + 'offset000', + { + method: 'post', + timeZone: 'America/Los_Angeles', + userLocale: 'en-US', + }, + function(err, records, offset) { + expect(err).toBeNull(); + expect(records.length).toBe(1); + expect(records[0].getId()).toBe('recordA'); + expect(records[0].get('Name')).toBe('Rebecca'); + expect(offset).toBe('offsetABC'); + done(); + } + ); + }); + + it('can throw an error if POST listRow fails', function(done) { + testExpressApp.set('handler override', function(req, res) { + expect(req.method).toBe('POST'); + // Request url should not include and query params and should + // use /listRecords endpoint with request body instead + expect(req.url).toBe( + '/v0/app123/Table/listRecords?timeZone=America%2FLos_Angeles&userLocale=en-US' + ); + + res.status(402).json({ + error: {message: 'foo bar'}, + }); + }); + + // Mock formula that is 15000 characters in length + const longFormula = [...Array(657)] + .map((_, i) => { + return `NOT({Name} = '${i}') & `; + }) + .join(); + + expect(longFormula.length).toBe(15000); + + return airtable + .base('app123') + .table('Table') + .list( + 50, + 'offset000', + { + filterByFormula: longFormula, + timeZone: 'America/Los_Angeles', + userLocale: 'en-US', + }, + function(err, records, offset) { + expect(err.statusCode).toBe(402); + expect(err.message).toBe('foo bar'); + expect(records).toBeUndefined(); + expect(offset).toBeUndefined(); + done(); + } + ); + }); }); diff --git a/test/select.test.js b/test/select.test.js index d68b2269..f361c90c 100644 --- a/test/select.test.js +++ b/test/select.test.js @@ -544,4 +544,81 @@ describe('record selection', function() { done(); }); }); + + it('uses POST listRows endpoint when params exceed 15k characters', function(done) { + // Mock formula that is 15000 characters in length + const longFormula = [...Array(657)] + .map((_, i) => { + return `NOT({Name} = '${i}') & `; + }) + .join(); + + expect(longFormula.length).toBe(15000); + + testExpressApp.set('handler override', function(req, res) { + expect(req.method).toBe('POST'); + expect(req.url).toBe( + '/v0/app123/Table/listRecords?timeZone=America%2FLos_Angeles&userLocale=en-US' + ); + expect(req.body.filterByFormula).toBe(longFormula); + + res.json({ + records: [ + { + id: 'recordA', + fields: {Name: 'Rebecca'}, + createdTime: '2020-04-20T16:20:00.000Z', + }, + ], + offset: 'offsetABC', + }); + }); + + return airtable + .base('app123') + .table('Table') + .select({ + filterByFormula: longFormula, + timeZone: 'America/Los_Angeles', + userLocale: 'en-US', + }) + .eachPage(function page(records) { + records.forEach(function(record) { + expect(record.id).toBe('recordA'); + expect(record.get('Name')).toBe('Rebecca'); + }); + done(); + }); + }); + + it('uses POST listRows endpoint when "post" is specified for method', function(done) { + testExpressApp.set('handler override', function(req, res) { + expect(req.method).toBe('POST'); + expect(req.url).toBe('/v0/app123/Table/listRecords?'); + res.json({ + records: [ + { + id: 'recordA', + fields: {Name: 'Rebecca'}, + createdTime: '2020-04-20T16:20:00.000Z', + }, + ], + offset: 'offsetABC', + }); + }); + + return airtable + .base('app123') + .table('Table') + .select({ + method: 'post', + }) + .eachPage(function page(records) { + records.forEach(function(record) { + expect(record.id).toBe('recordA'); + expect(record.get('Name')).toBe('Rebecca'); + }); + done(); + }); + }); }); diff --git a/test/test_files/airtable.browser.js b/test/test_files/airtable.browser.js index edddb42a..72b79d33 100644 --- a/test/test_files/airtable.browser.js +++ b/test/test_files/airtable.browser.js @@ -443,7 +443,7 @@ module.exports = objectToQueryParamString; },{"lodash/isArray":79,"lodash/isNil":85,"lodash/keys":93}],12:[function(require,module,exports){ "use strict"; -module.exports = "0.11.4"; +module.exports = "0.11.5"; },{}],13:[function(require,module,exports){ "use strict"; @@ -467,6 +467,7 @@ var record_1 = __importDefault(require("./record")); var callback_to_promise_1 = __importDefault(require("./callback_to_promise")); var has_1 = __importDefault(require("./has")); var query_params_1 = require("./query_params"); +var object_to_query_param_string_1 = __importDefault(require("./object_to_query_param_string")); /** * Builds a query object. Won't fetch until `firstPage` or * or `eachPage` is called. @@ -554,10 +555,40 @@ function eachPage(pageCallback, done) { if (!isFunction_1.default(done) && done !== void 0) { throw new Error('The second parameter to `eachPage` must be a function or undefined'); } - var path = "/" + this._table._urlEncodedNameOrId(); var params = __assign({}, this._params); + var pathAndParamsAsString = "/" + this._table._urlEncodedNameOrId() + "?" + object_to_query_param_string_1.default(params); + var queryParams = {}; + var requestData = null; + var method; + var path; + if (params.method === 'post' || pathAndParamsAsString.length > query_params_1.URL_CHARACTER_LENGTH_LIMIT) { + // There is a 16kb limit on GET requests. Since the URL makes up nearly all of the request size, we check for any requests that + // that come close to this limit and send it as a POST instead. Additionally, we'll send the request as a post if it is specified + // with the request params + requestData = params; + method = 'post'; + path = "/" + this._table._urlEncodedNameOrId() + "/listRecords"; + var paramNames = Object.keys(params); + for (var _i = 0, paramNames_1 = paramNames; _i < paramNames_1.length; _i++) { + var paramName = paramNames_1[_i]; + if (query_params_1.shouldListRecordsParamBePassedAsParameter(paramName)) { + // timeZone and userLocale is parsed from the GET request separately from the other params. This parsing + // does not occurring within the body parser we use for POST requests, so this will still need to be passed + // via query params + queryParams[paramName] = params[paramName]; + } + else { + requestData[paramName] = params[paramName]; + } + } + } + else { + method = 'get'; + queryParams = params; + path = "/" + this._table._urlEncodedNameOrId(); + } var inner = function () { - _this._table._base.runAction('get', path, params, null, function (err, response, result) { + _this._table._base.runAction(method, path, queryParams, requestData, function (err, response, result) { if (err) { done(err, null); } @@ -603,13 +634,13 @@ function all(done) { } module.exports = Query; -},{"./callback_to_promise":4,"./has":8,"./query_params":14,"./record":15,"lodash/isFunction":83,"lodash/keys":93}],14:[function(require,module,exports){ +},{"./callback_to_promise":4,"./has":8,"./object_to_query_param_string":11,"./query_params":14,"./record":15,"lodash/isFunction":83,"lodash/keys":93}],14:[function(require,module,exports){ "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.paramValidators = void 0; +exports.shouldListRecordsParamBePassedAsParameter = exports.URL_CHARACTER_LENGTH_LIMIT = exports.paramValidators = void 0; var typecheck_1 = __importDefault(require("./typecheck")); var isString_1 = __importDefault(require("lodash/isString")); var isNumber_1 = __importDefault(require("lodash/isNumber")); @@ -634,8 +665,15 @@ exports.paramValidators = { }, 'the value for `cellFormat` should be "json" or "string"'), timeZone: typecheck_1.default(isString_1.default, 'the value for `timeZone` should be a string'), userLocale: typecheck_1.default(isString_1.default, 'the value for `userLocale` should be a string'), + method: typecheck_1.default(function (method) { + return isString_1.default(method) && ['get', 'post'].includes(method); + }, 'the value for `method` should be "get" or "post"'), returnFieldsByFieldId: typecheck_1.default(isBoolean_1.default, 'the value for `returnFieldsByFieldId` should be a boolean'), }; +exports.URL_CHARACTER_LENGTH_LIMIT = 15000; +exports.shouldListRecordsParamBePassedAsParameter = function (paramName) { + return paramName === 'timeZone' || paramName === 'userLocale'; +}; },{"./typecheck":18,"lodash/isBoolean":81,"lodash/isNumber":86,"lodash/isPlainObject":89,"lodash/isString":90}],15:[function(require,module,exports){ "use strict"; @@ -840,6 +878,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) { var isPlainObject_1 = __importDefault(require("lodash/isPlainObject")); var deprecate_1 = __importDefault(require("./deprecate")); var query_1 = __importDefault(require("./query")); +var query_params_1 = require("./query_params"); +var object_to_query_param_string_1 = __importDefault(require("./object_to_query_param_string")); var record_1 = __importDefault(require("./record")); var callback_to_promise_1 = __importDefault(require("./callback_to_promise")); var Table = /** @class */ (function () { @@ -978,15 +1018,42 @@ var Table = /** @class */ (function () { record.destroy(done); } }; - Table.prototype._listRecords = function (limit, offset, opts, done) { + Table.prototype._listRecords = function (pageSize, offset, opts, done) { var _this = this; if (!done) { done = opts; opts = {}; } - var listRecordsParameters = __assign({ limit: limit, - offset: offset }, opts); - this._base.runAction('get', "/" + this._urlEncodedNameOrId() + "/", listRecordsParameters, null, function (err, response, results) { + var pathAndParamsAsString = "/" + this._urlEncodedNameOrId() + "?" + object_to_query_param_string_1.default(opts); + var path; + var listRecordsParameters = {}; + var listRecordsData = null; + var method; + if ((typeof opts !== 'function' && opts.method === 'post') || + pathAndParamsAsString.length > query_params_1.URL_CHARACTER_LENGTH_LIMIT) { + // // There is a 16kb limit on GET requests. Since the URL makes up nearly all of the request size, we check for any requests that + // that come close to this limit and send it as a POST instead. Additionally, we'll send the request as a post if it is specified + // with the request params + path = "/" + this._urlEncodedNameOrId() + "/listRecords"; + listRecordsData = __assign(__assign({}, (pageSize && { pageSize: pageSize })), (offset && { offset: offset })); + method = 'post'; + var paramNames = Object.keys(opts); + for (var _i = 0, paramNames_1 = paramNames; _i < paramNames_1.length; _i++) { + var paramName = paramNames_1[_i]; + if (query_params_1.shouldListRecordsParamBePassedAsParameter(paramName)) { + listRecordsParameters[paramName] = opts[paramName]; + } + else { + listRecordsData[paramName] = opts[paramName]; + } + } + } + else { + method = 'get'; + path = "/" + this._urlEncodedNameOrId() + "/"; + listRecordsParameters = __assign({ limit: pageSize, offset: offset }, opts); + } + this._base.runAction(method, path, listRecordsParameters, listRecordsData, function (err, response, results) { if (err) { done(err); return; @@ -1030,7 +1097,7 @@ var Table = /** @class */ (function () { }()); module.exports = Table; -},{"./callback_to_promise":4,"./deprecate":5,"./query":13,"./record":15,"lodash/isPlainObject":89}],18:[function(require,module,exports){ +},{"./callback_to_promise":4,"./deprecate":5,"./object_to_query_param_string":11,"./query":13,"./query_params":14,"./record":15,"lodash/isPlainObject":89}],18:[function(require,module,exports){ "use strict"; /* eslint-enable @typescript-eslint/no-explicit-any */ function check(fn, error) {