Skip to content

Commit

Permalink
Merge pull request #326 from Airtable/v0.11.5
Browse files Browse the repository at this point in the history
v0.11.5
  • Loading branch information
JamesMoody-at authored Sep 22, 2022
2 parents 97cc08e + da36c28 commit ccefdcc
Show file tree
Hide file tree
Showing 10 changed files with 591 additions and 54 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
87 changes: 77 additions & 10 deletions build/airtable.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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.
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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"));
Expand All @@ -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";
Expand Down Expand Up @@ -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 () {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
84 changes: 65 additions & 19 deletions src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -150,31 +156,71 @@ function eachPage<TFields extends FieldSet>(
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();
Expand Down
11 changes: 11 additions & 0 deletions src/query_params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TFields> {
field: keyof TFields;
direction?: 'asc' | 'desc';
Expand All @@ -63,5 +73,6 @@ export interface QueryParams<TFields> {
cellFormat?: 'json' | 'string';
timeZone?: string;
userLocale?: string;
method?: string;
returnFieldsByFieldId?: boolean;
}
Loading

0 comments on commit ccefdcc

Please sign in to comment.