310 lines
7.7 KiB
JavaScript
Executable File
310 lines
7.7 KiB
JavaScript
Executable File
var Backbone = require('backbone');
|
|
var QueryRowModel = require('./query-row-model');
|
|
var syncAbort = require('./backbone/sync-abort');
|
|
var _ = require('underscore');
|
|
var STATUS = require('./query-base-status');
|
|
|
|
var MAX_GET_LENGTH = 1024;
|
|
var WRAP_SQL_TEMPLATE = 'select <%= selectedColumns %> from (<%= sql %>) __wrapped';
|
|
var MAX_REPEATED_ERRORS = 3;
|
|
|
|
module.exports = Backbone.Collection.extend({
|
|
defaults: {
|
|
empty: true
|
|
},
|
|
|
|
DEFAULT_FETCH_OPTIONS: {
|
|
rows_per_page: 40,
|
|
sort_order: 'asc',
|
|
page: 0
|
|
},
|
|
|
|
// Due to a problem how Backbone checks if there is a duplicated model
|
|
// or not, we can't create the model with a function + its necessary options
|
|
model: QueryRowModel,
|
|
|
|
sync: syncAbort,
|
|
|
|
url: function () {
|
|
return this._configModel.getSqlApiUrl();
|
|
},
|
|
|
|
initialize: function (models, opts) {
|
|
opts = opts || {};
|
|
|
|
this._repeatedErrors = 0;
|
|
this._tableName = opts.tableName;
|
|
this._querySchemaModel = opts.querySchemaModel;
|
|
this._configModel = opts.configModel;
|
|
|
|
this.statusModel = new Backbone.Model({
|
|
status: STATUS.initial
|
|
});
|
|
|
|
this._initBinds();
|
|
},
|
|
|
|
_initBinds: function () {
|
|
this.listenTo(this._querySchemaModel, 'change:query', this._onQuerySchemaQueryChange);
|
|
|
|
if (this._querySchemaModel && this._querySchemaModel.columnsCollection) {
|
|
this.listenTo(this._querySchemaModel.columnsCollection, 'reset', this._onColumnsCollectionReset);
|
|
}
|
|
|
|
this.listenTo(this.statusModel, 'change:status', this._onStatusChanged);
|
|
},
|
|
|
|
_onColumnsCollectionReset: function () {
|
|
this._onQuerySchemaQueryChange();
|
|
|
|
if (this.shouldFetch()) {
|
|
this.fetch();
|
|
}
|
|
},
|
|
|
|
getStatusValue: function () {
|
|
return this.statusModel.get('status');
|
|
},
|
|
|
|
isInInitialStatus: function () {
|
|
return this.getStatusValue() === STATUS.initial;
|
|
},
|
|
|
|
isFetched: function () {
|
|
return this.getStatusValue() === STATUS.fetched;
|
|
},
|
|
|
|
isErrored: function () {
|
|
return this.getStatusValue() === STATUS.errored;
|
|
},
|
|
|
|
isFetching: function () {
|
|
return this.getStatusValue() === STATUS.fetching;
|
|
},
|
|
|
|
isDone: function () {
|
|
return this.isFetched() || this.isErrored();
|
|
},
|
|
|
|
isInFinalStatus: function () {
|
|
var finalStatuses = [STATUS.unavailable, STATUS.fetched, STATUS.errored];
|
|
return _.contains(finalStatuses, this.getStatusValue());
|
|
},
|
|
|
|
isUnavailable: function () {
|
|
return this.getStatusValue() === STATUS.unavailable;
|
|
},
|
|
|
|
canFetch: function () {
|
|
var hasQuery = !!this._querySchemaModel.get('query');
|
|
var isReady = this._querySchemaModel.get('ready');
|
|
var isFetched = this._querySchemaModel.isFetched();
|
|
var isErrored = this._querySchemaModel.isErrored();
|
|
|
|
return hasQuery && isReady && isFetched && !isErrored;
|
|
},
|
|
|
|
shouldFetch: function () {
|
|
return !this.isFetched() && !this.isFetching() && this.canFetch() && !this.isErrored();
|
|
},
|
|
|
|
resetFetch: function () {
|
|
this.statusModel.set('status', STATUS.unfetched);
|
|
},
|
|
|
|
isEmpty: function () {
|
|
throw new Error('QueryRowsCollection.isEmpty() is an async operation. Use `.isEmptyAsync` instead.');
|
|
},
|
|
|
|
isEmptyAsync: function () {
|
|
if (this.isInFinalStatus()) {
|
|
return Promise.resolve(this.size() === 0);
|
|
} else {
|
|
return new Promise(function (resolve) {
|
|
this.listenToOnce(this, 'inFinalStatus', function () {
|
|
resolve(this.size() === 0);
|
|
});
|
|
}.bind(this));
|
|
}
|
|
},
|
|
|
|
_onStatusChanged: function () {
|
|
if (this.isInFinalStatus()) {
|
|
this.trigger('inFinalStatus');
|
|
}
|
|
},
|
|
|
|
_onQuerySchemaQueryChange: function () {
|
|
this.statusModel.set('status', 'unfetched');
|
|
this.reset([], { silent: true });
|
|
},
|
|
|
|
_geometryColumnSQL: function (column) {
|
|
/* eslint-disable */
|
|
return [
|
|
"CASE",
|
|
"WHEN GeometryType(" + column + ") = 'POINT' THEN",
|
|
"ST_AsGeoJSON(" + column + ",8)",
|
|
"WHEN (" + column + " IS NULL) THEN",
|
|
"NULL",
|
|
"ELSE",
|
|
"GeometryType(" + column + ")",
|
|
"END " + column
|
|
].join(' ');
|
|
/* eslint-enable */
|
|
},
|
|
|
|
// return wrapped SQL removing the_geom and the_geom_webmercator
|
|
// to avoid fetching those columns.
|
|
// So for a sql like
|
|
// select * from table the returned value is
|
|
// select column1, column2, column3... from table
|
|
_getWrappedSQL: function (excludeColumns) {
|
|
var self = this;
|
|
var schema = this._querySchemaModel.columnsCollection.toJSON();
|
|
|
|
var selectedColumns = _
|
|
.chain(schema)
|
|
.omit(function (item) {
|
|
return _.contains(excludeColumns, item.name);
|
|
})
|
|
.map(function (item, index) {
|
|
if (item.type === 'geometry') {
|
|
return self._geometryColumnSQL(item.name);
|
|
}
|
|
return '"' + item.name + '"';
|
|
})
|
|
.value()
|
|
.join(',');
|
|
|
|
return _.template(WRAP_SQL_TEMPLATE)({
|
|
selectedColumns: selectedColumns,
|
|
sql: this._querySchemaModel.get('query')
|
|
});
|
|
},
|
|
|
|
_httpMethod: function (query) {
|
|
return query.length > MAX_GET_LENGTH ? 'POST' : 'GET';
|
|
},
|
|
|
|
_incrementRepeatedError: function () {
|
|
this._repeatedErrors++;
|
|
},
|
|
|
|
_resetRepeatedError: function () {
|
|
this._repeatedErrors = 0;
|
|
},
|
|
|
|
hasRepeatedErrors: function () {
|
|
return this._repeatedErrors >= MAX_REPEATED_ERRORS;
|
|
},
|
|
|
|
fetch: function (opts) {
|
|
if (this.isFetching()) return;
|
|
|
|
opts = opts || {};
|
|
|
|
var previousStatus = this.getStatusValue();
|
|
this.statusModel.set('status', STATUS.fetching);
|
|
|
|
var excludeColumns = (opts.data && opts.data.exclude) || [];
|
|
var query = this._getWrappedSQL(excludeColumns);
|
|
|
|
opts = opts || {};
|
|
opts.data = _.extend(
|
|
{},
|
|
this.DEFAULT_FETCH_OPTIONS,
|
|
opts.data && _.omit(opts.data, 'exclude') || {},
|
|
{
|
|
api_key: this._configModel.get('api_key'),
|
|
q: query
|
|
}
|
|
);
|
|
|
|
opts.method = this._httpMethod(query);
|
|
var errorCallback = opts.error;
|
|
opts.error = function (coll, resp) {
|
|
if (previousStatus === STATUS.unavailable) {
|
|
this._incrementRepeatedError();
|
|
}
|
|
|
|
if (resp && resp.error) {
|
|
this._querySchemaModel.setError(resp.error);
|
|
}
|
|
|
|
this.trigger('fail', coll, resp);
|
|
this.statusModel.set('status', this.hasRepeatedErrors() ? STATUS.errored : STATUS.unavailable);
|
|
errorCallback && errorCallback(coll, resp);
|
|
}.bind(this);
|
|
|
|
var successCallback = opts.success;
|
|
opts.success = function (coll, resp, options) {
|
|
if (resp && resp.error) {
|
|
return opts.error.apply(this, arguments);
|
|
}
|
|
|
|
this._resetRepeatedError();
|
|
this.statusModel.set('status', STATUS.fetched);
|
|
successCallback && successCallback(coll, resp);
|
|
}.bind(this);
|
|
|
|
// Needed to reset the whole collection when a fetch is done
|
|
opts.reset = true;
|
|
|
|
return Backbone.Collection.prototype.fetch.call(this, opts);
|
|
},
|
|
|
|
parse: function (r) {
|
|
return this._parseWithID(r.rows);
|
|
},
|
|
|
|
reset: function (result, opts) {
|
|
var items = [];
|
|
|
|
if (result && result.rows) {
|
|
// If reset comes from a fetch, we need to parse the rows
|
|
items = result.rows;
|
|
} else {
|
|
// If it comes directly from a simple reset function
|
|
items = result;
|
|
}
|
|
|
|
Backbone.Collection.prototype.reset.apply(this, [this._parseWithID(items)]);
|
|
},
|
|
|
|
_parseWithID: function (array) {
|
|
return _.map(array, function (attrs) {
|
|
attrs.__id = _.uniqueId();
|
|
return attrs;
|
|
});
|
|
},
|
|
|
|
addRow: function (opts) {
|
|
opts = opts || {};
|
|
this.create(
|
|
{
|
|
__id: _.uniqueId()
|
|
},
|
|
_.extend(
|
|
opts,
|
|
{
|
|
wait: true,
|
|
parse: true
|
|
}
|
|
)
|
|
);
|
|
},
|
|
|
|
setTableName: function (name) {
|
|
if (!name) return;
|
|
|
|
if (this._tableName) {
|
|
this._tableName = name;
|
|
|
|
this.each(function (rowModel) {
|
|
rowModel._tableName = name;
|
|
});
|
|
}
|
|
}
|
|
});
|