Skip to content

Commit

Permalink
Table.select method with filtering by formula support
Browse files Browse the repository at this point in the history
Example:

    // The params object specifies the query options.
    // All parameters are optional.
    var params = {
        fields: ['Name', 'Days', 'Country'], // only return these fields.
        filterByFormula: 'Days > 3', // return records that satisfy formula.
        maxRecords: 10, // at most 10 records in total.
        pageSize: 5, // at most 5 records per request.
        sort: [{field: 'Name', direction: 'asc'}],
        view: 'Main View', // can return records from a specific view.
    };

    var query = base('Table 1').select(params);

    // If you're only interested in the first page of
    // results, you can use `firstPage`:
    query.firstPage(function(error, records) {
        // 'records' contains the records for the first page of results.
    });

    // If you want all results, you can page through them
    // using `eachPage`.
    query.eachPage(function(records, nextPage) {
        // 'records' contains the records for the current page.
        // call nextPage() to fetch the next page of records.
        // If there are more records to fetch, this function
        // will get called again. Otherwise, the `done` function
        // will get called.
    }, function done(error) {
        // Called if there's an error, or after all
        // records have been fetched (in this case, the
        // error parameter will be null).
    });

This commit deprecates Table.list and Table.forEach.
  • Loading branch information
Kasra Kyanzadeh committed Jan 29, 2016
1 parent 01ddac6 commit 08e6e76
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 9 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# v0.4.0

* Added `Table.select` for querying records in a table. It takes the
following optional parameters for sorting and filtering records:

fields: only include the specified fields in results.
filterByFormula: only include records that satisfy the formula.
maxRecords: at most, return this many records in total.
pageSize: at most, return this many records in each request.
sort: specify fields to use for sorting the records.
view: return records from a specific view, using the view order.

* Deprecated `Table.list` and `Table.forEach`. Use `Table.select` instead.

# v0.2.0

* Renamed Application to Base. This is a breaking change for the client, no changes in the API.
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ right, be sure to ping us on intercom or open a github issue.

Here's a few things that we'll expose in the API shortly:

* Referencing records by name or some other field.
* Transparently creating new multiple choice options when you update records.
* Querying for recent changes
* Receiving notifications about changed records
Expand Down
2 changes: 1 addition & 1 deletion lib/airtable.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ var assert = require('assert');

var Class = require('./class');
var Base = require('./base');
var Table = require('./table');
var Record = require('./record');
var Table = require('./table');

var Airtable = Class.extend({
init: function(opts) {
Expand Down
25 changes: 25 additions & 0 deletions lib/deprecate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
var didWarnForDeprecation = {};

/**
* Convenience function for marking a function as deprecated.
*
* Will emit a warning the first time that function is called.
*
* @param fn the function to mark as deprecated.
* @param key a unique key identifying the function.
* @param message the warning message.
*
* @return a wrapped function
*/
function deprecate(fn, key, message) {
return function() {
if (!didWarnForDeprecation[key]) {
didWarnForDeprecation[key] = true;
console.warn(message);
}
fn.apply(this, arguments);
};
}

module.exports = deprecate;

155 changes: 155 additions & 0 deletions lib/query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
'use strict';

var assert = require('assert');
var _ = require('lodash');

var check = require('./typecheck');
var Class = require('./class');
var Record = require('./record');

var Query = Class.extend({
/**
* Builds a query object. Won't fetch until `firstPage` or
* or `eachPage` is called.
*/
init: function(table, params) {
assert(_.isPlainObject(params));
_.each(params, function(value, key) {
assert(Query.paramValidators[key] && Query.paramValidators[key](value).pass, 'Invalid parameter for Query: ' + key);
});

this._table = table;
this._params = params;
},

/**
* Fetches the first page of results for the query asynchronously,
* then calls `done(error, records)`.
*/
firstPage: function(done) {
assert(_.isFunction(done),
'The first parameter to `firstPage` must be a function');

this.eachPage(function(records, fetchNextPage) {
done(null, records);
}, function(error) {
done(error, null);
});
},

/**
* Fetches each page of results for the query asynchronously.
*
* Calls `pageCallback(records, fetchNextPage)` for each
* page. You must call `fetchNextPage()` to fetch the next page of
* results.
*
* After fetching all pages, or if there's an error, calls
* `done(error)`.
*/
eachPage: function(pageCallback, done) {
assert(_.isFunction(pageCallback),
'The first parameter to `eachPage` must be a function');

assert(_.isFunction(done) || _.isUndefined(done),
'The second parameter to `eachPage` must be a function or undefined');

var that = this;
var path = '/' + this._table._urlEncodedNameOrId();
var params = _.clone(this._params);

var inner = function() {
that._table._base.runAction('get', path, params, {}, function(err, response, result) {
if (err) {
done(err, null);
} else {
var next;
if (result.offset) {
params.offset = result.offset;
next = inner;
} else {
next = function() {
if (done) {
done(null);
}
};
}

var records = _.map(result.records, function(recordJson) {
return new Record(that, null, recordJson);
});

pageCallback(records, next);
}
});
};

inner();
},
});

Query.paramValidators = {
fields:
check(check.isArrayOf(_.isString), 'the value for `fields` should be an array of strings'),

filterByFormula:
check(_.isString, 'the value for `filterByFormula` should be a string'),

maxRecords:
check(_.isNumber, 'the value for `maxRecords` should be a number'),

pageSize:
check(_.isNumber, 'the value for `pageSize` should be a number'),

sort:
check(check.isArrayOf(function(obj) {
return (
_.isPlainObject(obj) &&
_.isString(obj.field) &&
(_.isUndefined(obj.direction) || _.contains(['asc', 'desc'], obj.direction))
);
}), 'the value for `sort` should be an array of sort objects. ' +
'Each sort object must have a string `field` value, and an optional ' +
'`direction` value that is "asc" or "desc".'
),

view:
check(_.isString, 'the value for `view` should be a string'),
};

/**
* Validates the parameters for passing to the Query constructor.
*
* @return an object with two keys:
* validParams: the object that should be passed to the constructor.
* ignoredKeys: a list of keys that will be ignored.
* errors: a list of error messages.
*/
Query.validateParams = function validateParams(params) {
assert(_.isPlainObject(params));

var validParams = {};
var ignoredKeys = [];
var errors = [];
_.each(params, function(value, key) {
if (Query.paramValidators.hasOwnProperty(key)) {
var validator = Query.paramValidators[key];
var validationResult = validator(value);
if (validationResult.pass) {
validParams[key] = value;
} else {
errors.push(validationResult.error);
}
} else {
ignoredKeys.push(key);
}
});

return {
validParams: validParams,
ignoredKeys: ignoredKeys,
errors: errors,
};
};

module.exports = Query;
54 changes: 48 additions & 6 deletions lib/table.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ var _ = require('lodash');
var assert = require('assert');
var async = require('async');

var AirtableError = require('./airtable_error');
var Class = require('./class');
var deprecate = require('./deprecate');
var Query = require('./query');
var Record = require('./record');
var AirtableError = require('./airtable_error');

var Table = Class.extend({
init: function(base, tableId, tableName) {
Expand All @@ -17,18 +19,58 @@ var Table = Class.extend({
this.name = tableName;

// Public API
this.find = this._record.bind(this);
this.find = this._findRecordById.bind(this);
this.select = this._selectRecords.bind(this);
this.create = this._createRecord.bind(this);
this.list = this._listRecords.bind(this);
this.forEach = this._forEachRecord.bind(this);
this.update = this._updateRecord.bind(this);
this.destroy = this._destroyRecord.bind(this);
this.replace = this._replaceRecord.bind(this);

// Deprecated API
this.list = deprecate(this._listRecords.bind(this),
'table.list',
'Airtable: `list()` is deprecated. Use `select()` instead.');
this.forEach = deprecate(this._forEachRecord.bind(this),
'table.forEach',
'Airtable: `forEach()` is deprecated. Use `select()` instead.');
},
_record: function(recordId, done) {
_findRecordById: function(recordId, done) {
var record = new Record(this, recordId);
record.fetch(done);
},
_selectRecords: function(params) {
if (_.isUndefined(params)) {
params = {};
}

if (arguments.length > 1) {
console.warn('Airtable: `select` takes only one parameter, but it was given '
+ arguments.length + ' parameters. '
+ 'Use `eachPage` or `firstPage` to fetch records.');
}

if (_.isPlainObject(params)) {
var validationResults = Query.validateParams(params);

if (validationResults.errors.length) {
var formattedErrors = validationResults.errors.map(function(error) {
return ' * ' + error;
});

assert(false, 'Airtable: invalid parameters for `select`:\n'
+ formattedErrors.join('\n'));
}

if (validationResults.ignoredKeys.length) {
console.warn('Airtable: the following parameters to `select` will be ignored: '
+ validationResults.ignoredKeys.join(', '));
}

return new Query(this, validationResults.validParams);
} else {
assert(false, 'Airtable: the parameter for `select` should be a plain object or undefined.');
}
},
_urlEncodedNameOrId: function(){
return this.id || encodeURIComponent(this.name);
},
Expand Down Expand Up @@ -92,7 +134,7 @@ var Table = Class.extend({
var offset = null;

var nextPage = function() {
that.list(limit, offset, opts, function(err, page, newOffset) {
that._listRecords(limit, offset, opts, function(err, page, newOffset) {
if (err) { done(err); return; }

_.each(page, callback);
Expand Down
23 changes: 23 additions & 0 deletions lib/typecheck.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
var _ = require('lodash');

function check(fn, error) {
return function(value) {
if (fn(value)) {
return {pass: true};
} else {
return {pass: false, error: error};
}
};
}

check.isOneOf = function isOneOf(options) {
return _.contains.bind(this, options);
};

check.isArrayOf = function(itemValidator) {
return function(value) {
return _.isArray(value) && _.every(value, itemValidator);
};
};

module.exports = check;
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.3.0",
"version": "0.4.0",
"homepage": "https://github.com/airtable/airtable.js",
"repository": "git://github.com/airtable/airtable.js.git",
"private": false,
Expand Down

0 comments on commit 08e6e76

Please sign in to comment.