@isrd-isi-edu/ermrestjs 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +55 -0
- package/dist/ermrest.d.ts +3481 -0
- package/dist/ermrest.js +45 -0
- package/dist/ermrest.js.gz +0 -0
- package/dist/ermrest.js.map +1 -0
- package/dist/ermrest.min.js +45 -0
- package/dist/ermrest.min.js.gz +0 -0
- package/dist/ermrest.min.js.map +1 -0
- package/dist/ermrest.ver.txt +1 -0
- package/dist/stats.html +4949 -0
- package/js/ag_reference.js +1483 -0
- package/js/core.js +4931 -0
- package/js/datapath.js +336 -0
- package/js/export.js +956 -0
- package/js/filters.js +192 -0
- package/js/format.js +344 -0
- package/js/hatrac.js +1130 -0
- package/js/json_ld_validator.js +285 -0
- package/js/parser.js +2320 -0
- package/js/setup/node.js +27 -0
- package/js/utils/helpers.js +2300 -0
- package/js/utils/json_ld_schema.js +680 -0
- package/js/utils/pseudocolumn_helpers.js +2196 -0
- package/package.json +79 -0
- package/src/index.ts +204 -0
- package/src/models/comment.ts +14 -0
- package/src/models/deferred-promise.ts +16 -0
- package/src/models/display-name.ts +5 -0
- package/src/models/errors.ts +408 -0
- package/src/models/path-prefix-alias-mapping.ts +130 -0
- package/src/models/reference/bulk-create-foreign-key-object.ts +133 -0
- package/src/models/reference/citation.ts +98 -0
- package/src/models/reference/contextualize.ts +535 -0
- package/src/models/reference/google-dataset-metadata.ts +72 -0
- package/src/models/reference/index.ts +14 -0
- package/src/models/reference/page.ts +520 -0
- package/src/models/reference/reference-aggregate-fn.ts +37 -0
- package/src/models/reference/reference.ts +2813 -0
- package/src/models/reference/related-reference.ts +467 -0
- package/src/models/reference/tuple.ts +652 -0
- package/src/models/reference-column/asset-pseudo-column.ts +498 -0
- package/src/models/reference-column/column-aggregate.ts +313 -0
- package/src/models/reference-column/facet-column.ts +1380 -0
- package/src/models/reference-column/foreign-key-pseudo-column.ts +626 -0
- package/src/models/reference-column/inbound-foreign-key-pseudo-column.ts +131 -0
- package/src/models/reference-column/index.ts +13 -0
- package/src/models/reference-column/key-pseudo-column.ts +236 -0
- package/src/models/reference-column/pseudo-column.ts +850 -0
- package/src/models/reference-column/reference-column.ts +740 -0
- package/src/models/source-object-node.ts +156 -0
- package/src/models/source-object-wrapper.ts +694 -0
- package/src/models/table-source-definitions.ts +98 -0
- package/src/services/authn.ts +43 -0
- package/src/services/catalog.ts +37 -0
- package/src/services/config.ts +202 -0
- package/src/services/error.ts +247 -0
- package/src/services/handlebars.ts +607 -0
- package/src/services/history.ts +136 -0
- package/src/services/http.ts +536 -0
- package/src/services/logger.ts +70 -0
- package/src/services/mustache.ts +0 -0
- package/src/utils/column-utils.ts +308 -0
- package/src/utils/constants.ts +526 -0
- package/src/utils/markdown-utils.ts +855 -0
- package/src/utils/reference-utils.ts +1658 -0
- package/src/utils/template-utils.ts +0 -0
- package/src/utils/type-utils.ts +89 -0
- package/src/utils/value-utils.ts +127 -0
- package/tsconfig.json +30 -0
- package/vite.config.mts +104 -0
|
@@ -0,0 +1,1483 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-this-alias */
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
3
|
+
/* eslint-disable prettier/prettier */
|
|
4
|
+
import moment from 'moment-timezone';
|
|
5
|
+
|
|
6
|
+
// models
|
|
7
|
+
import { MalformedURIError, NotFoundError } from '@isrd-isi-edu/ermrestjs/src/models/errors';
|
|
8
|
+
// import DeferredPromise from '@isrd-isi-edu/ermrestjs/src/models/deferred-promise';
|
|
9
|
+
|
|
10
|
+
// services
|
|
11
|
+
import ConfigService from '@isrd-isi-edu/ermrestjs/src/services/config';
|
|
12
|
+
import ErrorService from '@isrd-isi-edu/ermrestjs/src/services/error';
|
|
13
|
+
|
|
14
|
+
// utils
|
|
15
|
+
import { renderMarkdown } from '@isrd-isi-edu/ermrestjs/src/utils/markdown-utils';
|
|
16
|
+
import { isDefinedAndNotNull, isObject, isObjectAndNotNull, isStringAndNotEmpty, verify } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
|
|
17
|
+
import { fixedEncodeURIComponent } from '@isrd-isi-edu/ermrestjs/src/utils/value-utils';
|
|
18
|
+
import {
|
|
19
|
+
contextHeaderName,
|
|
20
|
+
_commentDisplayModes,
|
|
21
|
+
_dataFormats,
|
|
22
|
+
_HTMLColumnType,
|
|
23
|
+
URL_PATH_LENGTH_LIMIT,
|
|
24
|
+
} from '@isrd-isi-edu/ermrestjs/src/utils/constants';
|
|
25
|
+
|
|
26
|
+
// legacy
|
|
27
|
+
import { _convertSearchTermToFilter, _getSortModifier, _getPagingModifier } from '@isrd-isi-edu/ermrestjs/js/parser';
|
|
28
|
+
import { _isValidSortElement, _formatValueByType, _extends } from '@isrd-isi-edu/ermrestjs/js/utils/helpers';
|
|
29
|
+
import { _compressFacetObject } from '@isrd-isi-edu/ermrestjs/js/utils/pseudocolumn_helpers';
|
|
30
|
+
|
|
31
|
+
import { Type } from '@isrd-isi-edu/ermrestjs/js/core';
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Constructs a Reference object.
|
|
36
|
+
*
|
|
37
|
+
* This object will be the main object that client will interact with, when we want
|
|
38
|
+
* to use ermrset `attributegroup` api. Referencse are immutable and therefore can be
|
|
39
|
+
* safely passed around and used between multiple client components without risk that the
|
|
40
|
+
* underlying reference to server-side resources could change.
|
|
41
|
+
*
|
|
42
|
+
* Usage:
|
|
43
|
+
* - Clients can use this constructor to create attribute group references if needed.
|
|
44
|
+
* - This will currently be used by the aggregateGroup functions to return a
|
|
45
|
+
* AttributeGroupReference rather than a {@link ERMrest.Reference}
|
|
46
|
+
*
|
|
47
|
+
* @param {ERMRest.AttributeGroupColumn[]} keyColumns List of columns that will be used as keys for the attributegroup request.
|
|
48
|
+
* @param {?ERMRest.AttributeGroupColumn[]} aggregateColumns List of columns that will create the aggreagte columns list in the request.
|
|
49
|
+
* @param {ERMRest.AttributeGroupLocation} location The location object.
|
|
50
|
+
* @param {ERMRest.Catalog} catalog The catalog object.
|
|
51
|
+
* @param {ERMRest.Table} sourceTable The table object that represents this AG reference
|
|
52
|
+
* @param {String} context The context that this reference is used in
|
|
53
|
+
* @constructor
|
|
54
|
+
* @memberof ERMrest
|
|
55
|
+
*/
|
|
56
|
+
export function AttributeGroupReference(keyColumns, aggregateColumns, location, catalog, sourceTable, context) {
|
|
57
|
+
|
|
58
|
+
this.isAttributeGroup = true;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Array of AttributeGroupColumn that will be used as the key columns
|
|
62
|
+
* @type {AttributeGroupColumn[]}
|
|
63
|
+
*/
|
|
64
|
+
this._keyColumns = keyColumns;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Array of AttributeGroupColumn that will be used for the aggregate results
|
|
68
|
+
* @type {?ERMrest.AttributeGroupColumn[]}
|
|
69
|
+
*/
|
|
70
|
+
this._aggregateColumns = aggregateColumns;
|
|
71
|
+
|
|
72
|
+
this._allColumns = keyColumns.concat(aggregateColumns);
|
|
73
|
+
|
|
74
|
+
this.location = location;
|
|
75
|
+
|
|
76
|
+
this._server = catalog.server;
|
|
77
|
+
|
|
78
|
+
this._catalog = catalog;
|
|
79
|
+
|
|
80
|
+
this.table = sourceTable;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @type {ReferenceAggregateFn}
|
|
84
|
+
*/
|
|
85
|
+
this.aggregate = new AttributeGroupReferenceAggregateFn(this);
|
|
86
|
+
|
|
87
|
+
// column objects are created before the refernece.
|
|
88
|
+
// This makes sure that the columns are used in the context that reference is
|
|
89
|
+
// NOTE this is mutating the columns that are passed to it. Columns should not
|
|
90
|
+
// be shared between references with different contexts
|
|
91
|
+
this._context = isStringAndNotEmpty(context) ? context : '';
|
|
92
|
+
this._allColumns.forEach(function (c) {
|
|
93
|
+
c._setContext(context);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// this will force calling the uri api and therefore verifies the modifiers and location.
|
|
97
|
+
var uri = this.uri;
|
|
98
|
+
}
|
|
99
|
+
AttributeGroupReference.prototype = {
|
|
100
|
+
|
|
101
|
+
constructor: AttributeGroupReference,
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Visible columns
|
|
105
|
+
* @type {AttributeGroupColumn[]}
|
|
106
|
+
*/
|
|
107
|
+
get columns () {
|
|
108
|
+
if (this._columns === undefined) {
|
|
109
|
+
var self = this;
|
|
110
|
+
this._columns = [];
|
|
111
|
+
|
|
112
|
+
var addCol = function (col) {
|
|
113
|
+
if (col.visible) {
|
|
114
|
+
self._columns.push(col);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
this._keyColumns.forEach(addCol);
|
|
119
|
+
this._aggregateColumns.forEach(addCol);
|
|
120
|
+
|
|
121
|
+
}
|
|
122
|
+
return this._columns;
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Returns the visible key columns
|
|
127
|
+
* @type {AttributeGroupColumn[]}
|
|
128
|
+
*/
|
|
129
|
+
get shortestKey() {
|
|
130
|
+
return this._keyColumns.filter(function (kc) {
|
|
131
|
+
return kc.visible;
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
sort: function (sort) {
|
|
136
|
+
if (sort) {
|
|
137
|
+
verify((sort instanceof Array), "input should be an array");
|
|
138
|
+
verify(sort.every(_isValidSortElement), "invalid arguments in array");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// TODO doesn't support sort based on other columns.
|
|
142
|
+
var newLocation = this.location.changeSort(sort);
|
|
143
|
+
return new AttributeGroupReference(this._keyColumns, this._aggregateColumns, newLocation, this._catalog, this.table, this._context);
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
search: function (term) {
|
|
147
|
+
if (term) {
|
|
148
|
+
verify(typeof term === "string", "Invalid argument");
|
|
149
|
+
term = term.trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
verify(typeof this.location.searchColumn === "string" && this.location.searchColumn.length > 0, "Location object doesnt have search column.");
|
|
153
|
+
|
|
154
|
+
var newLocation = this.location.changeSearchTerm(term);
|
|
155
|
+
return new AttributeGroupReference(this._keyColumns, this._aggregateColumns, newLocation, this._catalog, this.table, this._context);
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The attributegroup uri.
|
|
160
|
+
* <service>/catalog/<_catalogId>/attributegroup/<path>/<search>/<_keyColumns>;<_aggregateColumns><sort><page>
|
|
161
|
+
*
|
|
162
|
+
* NOTE:
|
|
163
|
+
* - Since this is the object that has knowledge of columns, this should be here.
|
|
164
|
+
* (we might want to relocate it to the AttributeGroupLocation object.)
|
|
165
|
+
* - ermrest can processs this uri.
|
|
166
|
+
*
|
|
167
|
+
* @type {string}
|
|
168
|
+
*/
|
|
169
|
+
get uri () {
|
|
170
|
+
if (this._uri === undefined) {
|
|
171
|
+
var loc = this.location;
|
|
172
|
+
|
|
173
|
+
this._uri = [
|
|
174
|
+
loc.service, "catalog", loc.catalog.id, "attributegroup", this.ermrestPath
|
|
175
|
+
].join("/");
|
|
176
|
+
}
|
|
177
|
+
return this._uri;
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* This will generate a new unfiltered reference each time.
|
|
182
|
+
* Returns a reference that points to all entities of current table
|
|
183
|
+
*
|
|
184
|
+
* @type {Reference}
|
|
185
|
+
*/
|
|
186
|
+
get unfilteredReference() {
|
|
187
|
+
verify(this.table, "table is not defined for current reference");
|
|
188
|
+
var newLocation = new AttributeGroupLocation(this.location.service, this.location.catalog, [fixedEncodeURIComponent(this.table.schema.name),fixedEncodeURIComponent(this.table.name)].join(":"));
|
|
189
|
+
return new AttributeGroupReference(this._keyColumns, this._aggregateColumns, newLocation, this._catalog, this.table, this._context);
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The second part of Attributegroup uri.
|
|
194
|
+
* <path>/<search>/<_keyColumns>;<_aggregateColumns><sort><page>
|
|
195
|
+
*
|
|
196
|
+
* NOTE:
|
|
197
|
+
* - Since this is the object that has knowledge of columns, this should be here.
|
|
198
|
+
* (we might want to relocate it to the AttributeGroupLocation object.)
|
|
199
|
+
* - ermrest can processs this uri.
|
|
200
|
+
*
|
|
201
|
+
* @type {string}
|
|
202
|
+
*/
|
|
203
|
+
get ermrestPath () {
|
|
204
|
+
if (this._ermrestPath === undefined) {
|
|
205
|
+
var loc = this.location, self = this;
|
|
206
|
+
|
|
207
|
+
// given an array of columns, return col1,col2,col3
|
|
208
|
+
var colString = function (colArray) {
|
|
209
|
+
return colArray.map(function (col) {
|
|
210
|
+
return col.toString();
|
|
211
|
+
}).join(",");
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
var keyColumns = self._keyColumns.map(function (col) {
|
|
215
|
+
return col.toString();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// generate the url
|
|
219
|
+
var uri = loc.path;
|
|
220
|
+
|
|
221
|
+
if (typeof loc.searchFilter === "string" && loc.searchFilter.length > 0) {
|
|
222
|
+
uri += "/" + loc.searchFilter;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
uri += "/";
|
|
226
|
+
|
|
227
|
+
// make sure page object and sort are compatible
|
|
228
|
+
if ((loc.afterObject && loc.afterObject.length !== self.ermrestSortObject.length) ||
|
|
229
|
+
(loc.beforeObject && loc.beforeObject.length !== self.ermrestSortObject.length)) {
|
|
230
|
+
throw new MalformedURIError("The given page options are not compatible with sort criteria (Attributegroup Reference).");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// add extra sort columns to the key
|
|
234
|
+
self.ermrestSortObject.forEach(function (so) {
|
|
235
|
+
// if the column doesn't exist, add it
|
|
236
|
+
var k = self._allColumns.filter(function (col) {
|
|
237
|
+
return col.name == so.column;
|
|
238
|
+
})[0];
|
|
239
|
+
|
|
240
|
+
if (k) return;
|
|
241
|
+
|
|
242
|
+
keyColumns.push(
|
|
243
|
+
fixedEncodeURIComponent(so.column) + ":=" + fixedEncodeURIComponent(so.term)
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
uri += keyColumns.join(",");
|
|
248
|
+
|
|
249
|
+
// add aggregate columns
|
|
250
|
+
if (self._aggregateColumns.length !== 0) {
|
|
251
|
+
uri += ";" + colString(self._aggregateColumns);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// add sort
|
|
255
|
+
if (self.ermrestSortObject.length > 0) {
|
|
256
|
+
uri += _getSortModifier(self.ermrestSortObject);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// add page
|
|
260
|
+
if (loc.paging && loc.paging.length > 0) {
|
|
261
|
+
uri += loc.paging;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
self._ermrestPath = uri;
|
|
265
|
+
}
|
|
266
|
+
return this._ermrestPath;
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
get ermrestSortObject() {
|
|
270
|
+
if (this._ermrestSortObject === undefined) {
|
|
271
|
+
var self = this, loc = this.location;
|
|
272
|
+
|
|
273
|
+
self._ermrestSortObject = [];
|
|
274
|
+
if (Array.isArray(loc.sortObject)) {
|
|
275
|
+
// given a column name, tries to find it in the list of column (by term),
|
|
276
|
+
// if not found, it will add it by appending a alias to it.
|
|
277
|
+
// It will also return the used alias.
|
|
278
|
+
var alias = 0;
|
|
279
|
+
var allColumnNames = self._allColumns.map(function (col) {
|
|
280
|
+
return col.name;
|
|
281
|
+
});
|
|
282
|
+
var getAlias = function (colName) {
|
|
283
|
+
var col = self._allColumns.filter(function (c) {
|
|
284
|
+
return decodeURIComponent(c.term) === colName;
|
|
285
|
+
})[0];
|
|
286
|
+
|
|
287
|
+
// column is not in the list of defined columns, we should add it
|
|
288
|
+
if (col === undefined) {
|
|
289
|
+
while (allColumnNames.indexOf(alias.toString()) !== -1) {
|
|
290
|
+
++alias;
|
|
291
|
+
}
|
|
292
|
+
return (alias++).toString();
|
|
293
|
+
}
|
|
294
|
+
return col.name;
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
var col, sortColNames = {}, addedCols = {}, desc, name;
|
|
298
|
+
loc.sortObject.forEach(function (so) {
|
|
299
|
+
|
|
300
|
+
// find the oclumn
|
|
301
|
+
try {
|
|
302
|
+
col = self.getColumnByName(so.column);
|
|
303
|
+
} catch(e) {
|
|
304
|
+
throw new MalformedURIError("The sort criteria is invalid. Column `" + so.column +"` was not found (Attributegroup Reference).");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// don't add duplciates
|
|
308
|
+
if (col.name in sortColNames) return;
|
|
309
|
+
sortColNames[col.name] = true;
|
|
310
|
+
|
|
311
|
+
// make sure column is sortable
|
|
312
|
+
if(!col.sortable) {
|
|
313
|
+
throw new MalformedURIError("column '" + col.name + "' is not sortable (Attributegroup)");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// go through list of sort columns and add them to the key
|
|
317
|
+
col._sortColumns.forEach(function (sc) {
|
|
318
|
+
if (sc.column in addedCols) return;
|
|
319
|
+
addedCols[sc.column] = true;
|
|
320
|
+
|
|
321
|
+
// add to key columns if needed (won't add duplicates)
|
|
322
|
+
name = getAlias(sc.column);
|
|
323
|
+
|
|
324
|
+
desc = (so.descending === true);
|
|
325
|
+
if (sc.descending) desc = !desc;
|
|
326
|
+
|
|
327
|
+
// add to sort criteria
|
|
328
|
+
self._ermrestSortObject.push({
|
|
329
|
+
column: name,
|
|
330
|
+
descending: desc,
|
|
331
|
+
term: sc.column
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return this._ermrestSortObject;
|
|
338
|
+
},
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
*
|
|
342
|
+
* @param {int=} limit
|
|
343
|
+
* @param {Object=} contextHeaderParams the object that we want to log.
|
|
344
|
+
* @param {Boolean=} dontCorrectPage whether we should modify the page.
|
|
345
|
+
* If there's a @before in url and the number of results is less than the
|
|
346
|
+
* given limit, we will remove the @before and run the read again. Setting
|
|
347
|
+
* dontCorrectPage to true, will not do this extra check.
|
|
348
|
+
* @return {ERMRest.AttributeGroupPage}
|
|
349
|
+
*/
|
|
350
|
+
read: function (limit, contextHeaderParams, dontCorrectPage) {
|
|
351
|
+
try {
|
|
352
|
+
var defer = ConfigService.q.defer();
|
|
353
|
+
var hasPaging = (typeof limit === "number" && limit > 0);
|
|
354
|
+
|
|
355
|
+
var uri = this.uri;
|
|
356
|
+
if (hasPaging) {
|
|
357
|
+
uri += "?limit=" + (limit+1);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
var currRef = this, action = "read";
|
|
361
|
+
if (!contextHeaderParams || !isObject(contextHeaderParams)) {
|
|
362
|
+
contextHeaderParams = {"action": action};
|
|
363
|
+
} else if (typeof contextHeaderParams.action === "string") {
|
|
364
|
+
action = contextHeaderParams.action;
|
|
365
|
+
}
|
|
366
|
+
var config = {
|
|
367
|
+
headers: this._generateContextHeader(contextHeaderParams, limit)
|
|
368
|
+
};
|
|
369
|
+
this._server.http.get(uri, config).then(function (response) {
|
|
370
|
+
|
|
371
|
+
//determine hasNext and hasPrevious
|
|
372
|
+
var hasPrevious, hasNext = false;
|
|
373
|
+
if (hasPaging) {
|
|
374
|
+
if (!currRef.location.paging) { // first page
|
|
375
|
+
hasPrevious = false;
|
|
376
|
+
hasNext = (response.data.length > limit);
|
|
377
|
+
} else if (currRef.location.beforeObject) { // has @before()
|
|
378
|
+
hasPrevious = (response.data.length > limit);
|
|
379
|
+
hasNext = true;
|
|
380
|
+
} else { // has @after()
|
|
381
|
+
hasPrevious = true;
|
|
382
|
+
hasNext = (response.data.length > limit);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Because read() reads one extra row to determine whether the new page has previous or next
|
|
387
|
+
// We need to remove those extra row of data from the result
|
|
388
|
+
if (response.data.length > limit) {
|
|
389
|
+
// if no paging or @after, remove last row
|
|
390
|
+
if (!currRef.location.beforeObject)
|
|
391
|
+
response.data.splice(response.data.length-1);
|
|
392
|
+
else // @before, remove first row
|
|
393
|
+
response.data.splice(0, 1);
|
|
394
|
+
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// create a page using the data
|
|
398
|
+
var page = new AttributeGroupPage(currRef, response.data, hasPrevious, hasNext);
|
|
399
|
+
|
|
400
|
+
// We are paging based on @before (user navigated backwards in the set of data)
|
|
401
|
+
// AND there is less data than limit implies (beginning of set)
|
|
402
|
+
// OR we got the right set of data (tuples.length == pageLimit) but there's no previous set (beginning of set)
|
|
403
|
+
if (dontCorrectPage !== true && currRef.location.beforeObject && (response.data.length < limit || !hasPrevious) ) {
|
|
404
|
+
// a new location without paging
|
|
405
|
+
var newLocation = currRef.location.changePage();
|
|
406
|
+
var referenceWithoutPaging = new AttributeGroupReference(currRef._keyColumns, currRef._aggregateColumns, newLocation, currRef._catalog, currRef.table, currRef._context);
|
|
407
|
+
|
|
408
|
+
// remove the function and replace it with auto-reload
|
|
409
|
+
contextHeaderParams.action = action.substring(0,action.lastIndexOf(";")+1) + "auto-reload";
|
|
410
|
+
referenceWithoutPaging.read(limit, contextHeaderParams).then(function rereadReference(rereadPage) {
|
|
411
|
+
defer.resolve(rereadPage);
|
|
412
|
+
}, function error(err) {
|
|
413
|
+
throw err;
|
|
414
|
+
});
|
|
415
|
+
} else {
|
|
416
|
+
defer.resolve(page);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
}).catch(function (response) {
|
|
420
|
+
var error = ErrorService.responseToError(response);
|
|
421
|
+
defer.reject(error);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
return defer.promise;
|
|
425
|
+
|
|
426
|
+
} catch (e) {
|
|
427
|
+
return ConfigService.q.reject(e);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
getAggregates: function (aggregateList, contextHeaderParams) {
|
|
433
|
+
var defer = ConfigService.q.defer();
|
|
434
|
+
var url;
|
|
435
|
+
var urlSet = [];
|
|
436
|
+
var loc = this.location;
|
|
437
|
+
|
|
438
|
+
// create the context header params for log
|
|
439
|
+
if (!contextHeaderParams || !isObject(contextHeaderParams)) {
|
|
440
|
+
contextHeaderParams = {"action": "aggregate"};
|
|
441
|
+
}
|
|
442
|
+
var config = {
|
|
443
|
+
headers: this._generateContextHeader(contextHeaderParams)
|
|
444
|
+
};
|
|
445
|
+
var baseUri = loc.path;
|
|
446
|
+
if (typeof loc.searchFilter === "string" && loc.searchFilter.length > 0) {
|
|
447
|
+
baseUri += "/" + loc.searchFilter;
|
|
448
|
+
}
|
|
449
|
+
baseUri += "/";
|
|
450
|
+
|
|
451
|
+
for (var i = 0; i < aggregateList.length; i++) {
|
|
452
|
+
var agg = aggregateList[i];
|
|
453
|
+
|
|
454
|
+
// if this is the first aggregate, begin with the baseUri
|
|
455
|
+
if (i === 0) {
|
|
456
|
+
url = baseUri;
|
|
457
|
+
} else {
|
|
458
|
+
url += ",";
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// if adding the next aggregate to the url will push it past url length limit, push url onto the urlSet and reset the working url
|
|
462
|
+
if ((url + i + ":=" + agg).length > URL_PATH_LENGTH_LIMIT) {
|
|
463
|
+
// strip off an extra ','
|
|
464
|
+
if (url.charAt(url.length-1) === ',') {
|
|
465
|
+
url = url.substring(0, url.length-1);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
urlSet.push(url);
|
|
469
|
+
url = baseUri;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// use i as the alias
|
|
473
|
+
url += i + ":=" + agg;
|
|
474
|
+
|
|
475
|
+
// We are at the end of the aggregate list
|
|
476
|
+
if (i+1 === aggregateList.length) {
|
|
477
|
+
urlSet.push(url);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
var aggregatePromises = [];
|
|
482
|
+
var http = this._server.http;
|
|
483
|
+
for (var j = 0; j < urlSet.length; j++) {
|
|
484
|
+
aggregatePromises.push(http.get(loc.service + "/catalog/" + loc.catalog.id + "/aggregate/" + urlSet[j], config));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
ConfigService.q.all(aggregatePromises).then(function getAggregates(response) {
|
|
488
|
+
// all response rows merged into one object
|
|
489
|
+
var singleResponse = {};
|
|
490
|
+
|
|
491
|
+
// collect all the data in one object so we can map it to an array
|
|
492
|
+
for (var k = 0; k < response.length; k++) {
|
|
493
|
+
Object.assign(singleResponse, response[k].data[0]);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
var responseArray = [];
|
|
497
|
+
for (var m = 0; m < aggregateList.length; m++) {
|
|
498
|
+
responseArray.push(singleResponse[m]);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
defer.resolve(responseArray);
|
|
502
|
+
}, function error(response) {
|
|
503
|
+
var error = ErrorService.responseToError(response);
|
|
504
|
+
return defer.reject(error);
|
|
505
|
+
}).catch(function (error) {
|
|
506
|
+
return defer.reject(error);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
return defer.promise;
|
|
510
|
+
},
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Find a column in list of key and aggregate columns.
|
|
514
|
+
* @param {string} name the column name
|
|
515
|
+
* @return {AttributeGroupColumn}
|
|
516
|
+
*/
|
|
517
|
+
getColumnByName: function (name) {
|
|
518
|
+
|
|
519
|
+
var findCol = function (list) {
|
|
520
|
+
for (let i = 0; i < list.length; i++) {
|
|
521
|
+
if (list[i].name === name) {
|
|
522
|
+
return list[i];
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return false;
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
var c = findCol(this._allColumns);
|
|
529
|
+
if (c) {
|
|
530
|
+
return c;
|
|
531
|
+
}
|
|
532
|
+
throw new NotFoundError("", "Column " + name + " not found.");
|
|
533
|
+
},
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* The default information that we want to be logged including catalog, schema_table, and facet (filter).
|
|
537
|
+
* @type {Object}
|
|
538
|
+
*/
|
|
539
|
+
get defaultLogInfo() {
|
|
540
|
+
var obj = {};
|
|
541
|
+
obj.catalog = this._catalog.id;
|
|
542
|
+
if (this.table) {
|
|
543
|
+
obj.schema_table = this.table.schema.name + ":" + this.table.name;
|
|
544
|
+
}
|
|
545
|
+
return obj;
|
|
546
|
+
},
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* The filter information that should be logged
|
|
550
|
+
* Currently only includes the search term.
|
|
551
|
+
* @type {Object}
|
|
552
|
+
*/
|
|
553
|
+
get filterLogInfo() {
|
|
554
|
+
var obj = {};
|
|
555
|
+
if (isObjectAndNotNull(this.location.searchObject) && typeof this.location.searchTerm === "string" && this.location.searchTerm) {
|
|
556
|
+
obj.filters = _compressFacetObject({"and": [{"source": "search-box", "search": [this.location.searchTerm]}]});
|
|
557
|
+
}
|
|
558
|
+
return obj;
|
|
559
|
+
},
|
|
560
|
+
|
|
561
|
+
_generateContextHeader: function (contextHeaderParams, page_size) {
|
|
562
|
+
if (!contextHeaderParams || !isObject(contextHeaderParams)) {
|
|
563
|
+
contextHeaderParams = {};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
for (var key in this.defaultLogInfo) {
|
|
567
|
+
// only add the values that are not defined.
|
|
568
|
+
if (key in contextHeaderParams) continue;
|
|
569
|
+
contextHeaderParams[key] = this.defaultLogInfo[key];
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (Number.isInteger(page_size)) {
|
|
573
|
+
contextHeaderParams.page_size = page_size;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
var headers = {};
|
|
577
|
+
headers[contextHeaderName] = contextHeaderParams;
|
|
578
|
+
return headers;
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* @namespace ERMrest.AttributeGroupPage
|
|
585
|
+
*/
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Constructs a AttributeGroupPage object. A _page_ represents a set of results returned from
|
|
589
|
+
* ERMrest. It may not represent the complete set of results. There is an
|
|
590
|
+
* iterator pattern used here, where its {@link ERMrest.AttributeGroupPage#previous} and
|
|
591
|
+
* {@link ERMrest.AttributeGroupPage#next} properties will give the client a
|
|
592
|
+
* {@link ERMrest.AttributeGroupReference} to the previous and next set of results,
|
|
593
|
+
* respectively.
|
|
594
|
+
*
|
|
595
|
+
* Usage:
|
|
596
|
+
* - Clients _do not_ directly access this constructor.
|
|
597
|
+
* - This will currently be used by the AggregateGroupReference to return a
|
|
598
|
+
* AttributeGroupPage rather than a {@link ERMrest.Page}
|
|
599
|
+
* See {@link ERMrest.AttributeGroupReference#read}.
|
|
600
|
+
*
|
|
601
|
+
* @param {ERMRest.AttributeGroupReference} reference aggregate reference representing the data for this page
|
|
602
|
+
* @param {!Object[]} data The data returned from ERMrest
|
|
603
|
+
* @param {Boolean} hasPrevious Whether database has some data before current page
|
|
604
|
+
* @param {Boolean} hasNext Whether database has some data after current page
|
|
605
|
+
* @constructor
|
|
606
|
+
* @memberof ERMrest
|
|
607
|
+
*/
|
|
608
|
+
export function AttributeGroupPage(reference, data, hasPrevious, hasNext) {
|
|
609
|
+
/**
|
|
610
|
+
* The page's associated reference.
|
|
611
|
+
* @type {AttributeGroupReference}
|
|
612
|
+
*/
|
|
613
|
+
this.reference = reference;
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Whether there is more entities before this page
|
|
617
|
+
* @returns {boolean}
|
|
618
|
+
*/
|
|
619
|
+
this.hasPrevious = hasPrevious;
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Whether there is more entities after this page
|
|
623
|
+
* @returns {boolean}
|
|
624
|
+
*/
|
|
625
|
+
this.hasNext = hasNext;
|
|
626
|
+
|
|
627
|
+
this._data = data;
|
|
628
|
+
}
|
|
629
|
+
AttributeGroupPage.prototype = {
|
|
630
|
+
constructor: AttributeGroupPage,
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* An array of processed tuples.
|
|
634
|
+
*
|
|
635
|
+
* Usage:
|
|
636
|
+
* ```
|
|
637
|
+
* for (var i=0, len=page.tuples.length; i<len; i++) {
|
|
638
|
+
* var tuple = page.tuples[i];
|
|
639
|
+
* console.log("Tuple:", tuple.displayname.value, "has values:", tuple.values);
|
|
640
|
+
* }
|
|
641
|
+
* ```
|
|
642
|
+
* @type {AttributeGroupTuple[]}
|
|
643
|
+
*/
|
|
644
|
+
get tuples () {
|
|
645
|
+
if (this._tuples === undefined) {
|
|
646
|
+
var self = this;
|
|
647
|
+
self._tuples = [];
|
|
648
|
+
this._data.forEach(function (data) {
|
|
649
|
+
self._tuples.push(new AttributeGroupTuple(self, data));
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
return this._tuples;
|
|
653
|
+
},
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* the page length (number of rows in the page)
|
|
657
|
+
* @type {integer}
|
|
658
|
+
*/
|
|
659
|
+
get length() {
|
|
660
|
+
if (this._length === undefined) {
|
|
661
|
+
this._length = this._data.length;
|
|
662
|
+
}
|
|
663
|
+
return this._length;
|
|
664
|
+
},
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* A reference to the next set of results.
|
|
668
|
+
*
|
|
669
|
+
* Usage:
|
|
670
|
+
* ```
|
|
671
|
+
* if (reference.next) {
|
|
672
|
+
* // more tuples in the 'next' direction are available
|
|
673
|
+
* reference.next.read(10).then(
|
|
674
|
+
* ...
|
|
675
|
+
* );
|
|
676
|
+
* }
|
|
677
|
+
* ```
|
|
678
|
+
* @type {AttributeGroupReference|null}
|
|
679
|
+
*/
|
|
680
|
+
get next() {
|
|
681
|
+
if (this.hasNext) {
|
|
682
|
+
return this._getSiblingReference(true);
|
|
683
|
+
}
|
|
684
|
+
return null;
|
|
685
|
+
},
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* A reference to the previous set of results.
|
|
689
|
+
*
|
|
690
|
+
* Usage:
|
|
691
|
+
* ```
|
|
692
|
+
* if (reference.previous) {
|
|
693
|
+
* // more tuples in the 'previous' direction are available
|
|
694
|
+
* reference.previous.read(10).then(
|
|
695
|
+
* ...
|
|
696
|
+
* );
|
|
697
|
+
* }
|
|
698
|
+
* ```
|
|
699
|
+
* @type {AttributeGroupReference|null}
|
|
700
|
+
*/
|
|
701
|
+
get previous() {
|
|
702
|
+
if (this.hasPrevious) {
|
|
703
|
+
return this._getSiblingReference(false);
|
|
704
|
+
}
|
|
705
|
+
return null;
|
|
706
|
+
},
|
|
707
|
+
|
|
708
|
+
// return a reference to the next or previous page.
|
|
709
|
+
_getSiblingReference: function (next) {
|
|
710
|
+
var self = this;
|
|
711
|
+
var currRef = this.reference;
|
|
712
|
+
var rows = [];
|
|
713
|
+
|
|
714
|
+
if (!Array.isArray(currRef.ermrestSortObject) || currRef.ermrestSortObject === 0) {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (!self._data || self._data.length === 0) {
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
var rowIndex = next ? self._data.length-1 : 0;
|
|
723
|
+
|
|
724
|
+
var pageValues = [], data = self._data[rowIndex], name;
|
|
725
|
+
for (var i = 0; i < currRef.ermrestSortObject.length; i++) {
|
|
726
|
+
name = currRef.ermrestSortObject[i].column;
|
|
727
|
+
pageValues.push(data[name]);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (pageValues === null) {
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
var newLocation;
|
|
735
|
+
if (next) {
|
|
736
|
+
newLocation = currRef.location.changePage(pageValues, null);
|
|
737
|
+
} else {
|
|
738
|
+
newLocation = currRef.location.changePage(null, pageValues);
|
|
739
|
+
}
|
|
740
|
+
return new AttributeGroupReference(currRef._keyColumns, currRef._aggregateColumns, newLocation, currRef._catalog, currRef.table, currRef._context);
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* @namespace ERMrest.AttributeGroupTuple
|
|
747
|
+
*/
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Constructs a new Tuple. In database jargon, a tuple is a row in a
|
|
751
|
+
* relation. This object represents a row returned by a query to ERMrest.
|
|
752
|
+
*
|
|
753
|
+
* Usage:
|
|
754
|
+
* Clients _do not_ directly access this constructor.
|
|
755
|
+
* See {@link ERMrest.AttributeGroupPage#tuples}.
|
|
756
|
+
*
|
|
757
|
+
* @param {!ERMrest.AttributeGroupPage} page The Page object from which this data was acquired.
|
|
758
|
+
* @param {!Object} data The unprocessed tuple of data returned from ERMrest.
|
|
759
|
+
* @constructor
|
|
760
|
+
* @memberof ERMrest
|
|
761
|
+
*/
|
|
762
|
+
export function AttributeGroupTuple(page, data) {
|
|
763
|
+
this._page = page;
|
|
764
|
+
this._data = data;
|
|
765
|
+
}
|
|
766
|
+
AttributeGroupTuple.prototype = {
|
|
767
|
+
constructor: AttributeGroupTuple,
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* The array of boolean values of this tuple speicifying the value is HTML or not. The ordering of the
|
|
771
|
+
* values in the array matches the ordering of the columns in the
|
|
772
|
+
* reference (see {@link ERMrest.Reference#columns}).
|
|
773
|
+
* TODO Eventually should be refactored (https://github.com/informatics-isi-edu/ermrestjs/issues/189).
|
|
774
|
+
*
|
|
775
|
+
* @type {boolean[]}
|
|
776
|
+
*/
|
|
777
|
+
get isHTML() {
|
|
778
|
+
if (this._isHTML === undefined) {
|
|
779
|
+
// will populate the this._isHTML
|
|
780
|
+
var value = this.values;
|
|
781
|
+
}
|
|
782
|
+
return this._isHTML;
|
|
783
|
+
},
|
|
784
|
+
|
|
785
|
+
get values() {
|
|
786
|
+
if (this._values === undefined) {
|
|
787
|
+
this._values = [];
|
|
788
|
+
this._isHTML = [];
|
|
789
|
+
|
|
790
|
+
var columns = this._page.reference.columns,
|
|
791
|
+
context = this._page.reference._context,
|
|
792
|
+
self = this, templateVariables = {}, k, v;
|
|
793
|
+
columns.forEach(function (col) {
|
|
794
|
+
if (!(col.name in self._data)) return;
|
|
795
|
+
k = col.name;
|
|
796
|
+
v = col.formatvalue(self._data[k], context);
|
|
797
|
+
templateVariables[k] = v;
|
|
798
|
+
templateVariables["_" + k] = self._data[k];
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
var presentation;
|
|
802
|
+
|
|
803
|
+
columns.forEach(function (col) {
|
|
804
|
+
presentation = col.formatPresentation(self._data, context, templateVariables);
|
|
805
|
+
self._values.push(presentation.value);
|
|
806
|
+
self._isHTML.push(presentation.isHTML);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
}
|
|
810
|
+
return this._values;
|
|
811
|
+
},
|
|
812
|
+
|
|
813
|
+
get data() {
|
|
814
|
+
return this._data;
|
|
815
|
+
},
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* The unique identifier for this tuple composed of the values for each
|
|
819
|
+
* of the shortest key columns concatenated together by an '_'
|
|
820
|
+
*
|
|
821
|
+
* @type {string}
|
|
822
|
+
*/
|
|
823
|
+
get uniqueId() {
|
|
824
|
+
if (this._uniqueId === undefined) {
|
|
825
|
+
var data = this._data, hasNull, self = this;
|
|
826
|
+
this._uniqueId = self._page.reference.shortestKey.reduce(function (res, c, index) {
|
|
827
|
+
hasNull = hasNull || data[c.name] == null;
|
|
828
|
+
const isJSON = c.type.name === 'json' || c.type.name === 'jsonb';
|
|
829
|
+
// if the column is JSON, we need to stringify it otherwise it will print [object Object]
|
|
830
|
+
const value = isJSON ? JSON.stringify(data[c.name]) : data[c.name];
|
|
831
|
+
return res + (index > 0 ? "_" : "") + value;
|
|
832
|
+
}, "");
|
|
833
|
+
|
|
834
|
+
//TODO should be evaluated for composite keys
|
|
835
|
+
// might need to change, but for the current usecase it's fine.
|
|
836
|
+
if (hasNull) {
|
|
837
|
+
this._uniqueId = null;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return this._uniqueId;
|
|
841
|
+
},
|
|
842
|
+
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* The _display name_ of this tuple. currently it will be values of
|
|
846
|
+
* key columns concatenated together by `_`.
|
|
847
|
+
*
|
|
848
|
+
* Usage:
|
|
849
|
+
* ```
|
|
850
|
+
* console.log("This tuple has a displayable name of ", tuple.displayname.value);
|
|
851
|
+
* ```
|
|
852
|
+
* @type {string}
|
|
853
|
+
*/
|
|
854
|
+
get displayname() {
|
|
855
|
+
if (this._displayname === undefined) {
|
|
856
|
+
var keyColumns = this._page.reference.shortestKey,
|
|
857
|
+
data = this._data,
|
|
858
|
+
self = this,
|
|
859
|
+
hasNull = false,
|
|
860
|
+
hasMarkdown = false,
|
|
861
|
+
values = [],
|
|
862
|
+
value;
|
|
863
|
+
|
|
864
|
+
keyColumns.forEach(function (c) {
|
|
865
|
+
hasNull = hasNull || data[c.name] == null;
|
|
866
|
+
if (hasNull) return;
|
|
867
|
+
|
|
868
|
+
hasMarkdown = hasMarkdown || (_HTMLColumnType.indexOf(c.type.name) != -1);
|
|
869
|
+
values.push(c.formatvalue(data[c.name], self._page.reference._context));
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
value = hasNull ? null: values.join(":");
|
|
873
|
+
|
|
874
|
+
this._displayname = {
|
|
875
|
+
"value": hasMarkdown ? renderMarkdown(value, true) : value,
|
|
876
|
+
"unformatted": value,
|
|
877
|
+
"isHTML": hasMarkdown
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
return this._displayname;
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Constructor for creating a column for creating a {@link ERMrest.AttributeGroupReference}
|
|
886
|
+
*
|
|
887
|
+
* NOTE If you're passing baseColumn, we're assuming that the location path is ending
|
|
888
|
+
* in the table that this column belongs too. This assumption exists for sort logic.
|
|
889
|
+
* This because we're looking at the column_order of the baseColumn and we're appending
|
|
890
|
+
* them to the key columns. Now if that column is not in the table, this will not work.
|
|
891
|
+
* If that is not the case, you need to disable the sort.
|
|
892
|
+
*
|
|
893
|
+
* @param {string} alias the alias that we want to use. If alias exist we will use the alias=term for creating url.
|
|
894
|
+
* @param {string} term the term string, e.g., cnt(*) or col1.
|
|
895
|
+
* @param {Column} baseColumn the database column that this is based on
|
|
896
|
+
* @param {any|string} displayname displayname of column, if it's an object it will have `value`, `unformatted`, and `isHTML`
|
|
897
|
+
* @param {any} colType type of column
|
|
898
|
+
* @param {string?} comment The string for comment (tooltip)
|
|
899
|
+
* @param {Boolean} sortable Whether the column is sortable
|
|
900
|
+
* @param {Boolean} visible Whether we want this column be returned in the tuples
|
|
901
|
+
* @constructor
|
|
902
|
+
*/
|
|
903
|
+
export function AttributeGroupColumn(alias, term, baseColumn, displayname, colType, comment, sortable, visible) {
|
|
904
|
+
/**
|
|
905
|
+
* The alias for the column.
|
|
906
|
+
* The alias might be undefined. If it's aggregate column and it has an aggregate function
|
|
907
|
+
* then this will be required by ermrest, but we're not checking anything here...
|
|
908
|
+
*
|
|
909
|
+
* @type {string}
|
|
910
|
+
* @private
|
|
911
|
+
*/
|
|
912
|
+
this._alias = alias;
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* This might include the aggregate functions. This is the right side of alias (alias:=term)
|
|
916
|
+
* NOTE:
|
|
917
|
+
* - This MUST be url encoded. We're not going to encode this.
|
|
918
|
+
* - We might want to seperate the aggreagte function and column, but right now this will only be used for
|
|
919
|
+
* creating the url.
|
|
920
|
+
* - Since it can include characters like `*`, we cannot encode this. We assume that
|
|
921
|
+
* this has been encoded before and we're just passing it to the ermrest.
|
|
922
|
+
* We might want to apply the same rule to every other places that we're passing the column names.
|
|
923
|
+
*
|
|
924
|
+
* @type {string}
|
|
925
|
+
*/
|
|
926
|
+
this.term = term;
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* The database column that this is based on. It might not be defined.
|
|
930
|
+
* @type {Column}
|
|
931
|
+
*/
|
|
932
|
+
this.baseColumn = baseColumn;
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
if (typeof displayname === 'string') {
|
|
936
|
+
this._displayname = {"value": displayname, "unformatted": displayname, "isHTML": false};
|
|
937
|
+
} else if (isObjectAndNotNull(displayname)){
|
|
938
|
+
this._displayname = displayname;
|
|
939
|
+
} else if (baseColumn) {
|
|
940
|
+
this._displayname = baseColumn.displayname;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
if (typeof colType === 'string') {
|
|
945
|
+
this.type = new Type({typename: colType});
|
|
946
|
+
} else if (baseColumn){
|
|
947
|
+
this.type = baseColumn.type;
|
|
948
|
+
} else {
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Type object
|
|
952
|
+
* @type {any}
|
|
953
|
+
*/
|
|
954
|
+
this.type = colType;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* tooltip
|
|
959
|
+
* @type {Object}
|
|
960
|
+
*/
|
|
961
|
+
this.comment = comment;
|
|
962
|
+
if (typeof comment === 'string') {
|
|
963
|
+
this.comment = {
|
|
964
|
+
isHTML: false,
|
|
965
|
+
value: comment,
|
|
966
|
+
unformatted: comment,
|
|
967
|
+
displayMode: _commentDisplayModes.tooltip
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (sortable === false) {
|
|
972
|
+
this._sortable = false;
|
|
973
|
+
this._sortColumns_cached = [];
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* We should have a concept of visible columns, this was the easiest way of implementing it to me.
|
|
978
|
+
* @type {boolean}
|
|
979
|
+
*/
|
|
980
|
+
this.visible = visible;
|
|
981
|
+
}
|
|
982
|
+
AttributeGroupColumn.prototype = {
|
|
983
|
+
constructor: AttributeGroupColumn,
|
|
984
|
+
|
|
985
|
+
toString: function () {
|
|
986
|
+
var res = "";
|
|
987
|
+
if (typeof this._alias === "string" && this._alias.length !== 0) {
|
|
988
|
+
res += fixedEncodeURIComponent(this._alias) + ":=";
|
|
989
|
+
}
|
|
990
|
+
res += this.term;
|
|
991
|
+
return res;
|
|
992
|
+
},
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* name of the column that is being used in projection list.
|
|
996
|
+
* If alias exists, it will return alias, otherwise the decoded version of term.
|
|
997
|
+
* @type {string}
|
|
998
|
+
*/
|
|
999
|
+
get name() {
|
|
1000
|
+
if (this._name === undefined) {
|
|
1001
|
+
if (typeof this._alias === "string" && this._alias.length !== 0) {
|
|
1002
|
+
this._name = this._alias;
|
|
1003
|
+
} else {
|
|
1004
|
+
this._name = decodeURIComponent(this.term);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return this._name;
|
|
1008
|
+
},
|
|
1009
|
+
|
|
1010
|
+
get displayname() {
|
|
1011
|
+
return this._displayname;
|
|
1012
|
+
},
|
|
1013
|
+
|
|
1014
|
+
formatvalue: function (data, context, options) {
|
|
1015
|
+
if (data === null || data === undefined) {
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (this.baseColumn) {
|
|
1020
|
+
return this.baseColumn.formatvalue(data, context, options);
|
|
1021
|
+
}
|
|
1022
|
+
return _formatValueByType(this.type, data, options);
|
|
1023
|
+
},
|
|
1024
|
+
|
|
1025
|
+
formatPresentation: function (data, context, templateVariables, options) {
|
|
1026
|
+
data = data || {};
|
|
1027
|
+
|
|
1028
|
+
var formattedValue = this.formatvalue(data[this.name], context, options);
|
|
1029
|
+
|
|
1030
|
+
/*
|
|
1031
|
+
* NOTE: currently will only return the given data. This function exist
|
|
1032
|
+
* so it will be the same pattern as Reference and Column apis.
|
|
1033
|
+
* Eventually this also will be used for a case that we want to return rowName,
|
|
1034
|
+
* Although in that case we need to have the Table object. We can pass the Table object
|
|
1035
|
+
* to this, but the next problem will be the name of columns. The keys in the data object
|
|
1036
|
+
* are aliases and not the actual column names in the table.
|
|
1037
|
+
*
|
|
1038
|
+
*/
|
|
1039
|
+
if (_HTMLColumnType.indexOf(this.type.name) != -1) {
|
|
1040
|
+
return {isHTML: true, value: renderMarkdown(formattedValue, true), unformatted: formattedValue};
|
|
1041
|
+
}
|
|
1042
|
+
return {isHTML: false, value: formattedValue, unformatted: formattedValue};
|
|
1043
|
+
},
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* @desc An array of objects that have `column` which is column name (this is different from other sortColumns),
|
|
1047
|
+
* and `descending` which is true/fals. The `descending` boolean indicates whether we should change the direction of sort or not.
|
|
1048
|
+
* The column name is going to be database column names (equivalent to this.term)
|
|
1049
|
+
*
|
|
1050
|
+
* - if sortable passed as false, it will be empty.
|
|
1051
|
+
* - if baseColumn exists, it will return the column order of baseColumn
|
|
1052
|
+
* - otherwise the current column
|
|
1053
|
+
* @type {Object[]}
|
|
1054
|
+
*/
|
|
1055
|
+
get _sortColumns() {
|
|
1056
|
+
if (this._sortColumns_cached === undefined) {
|
|
1057
|
+
this._determineSortable();
|
|
1058
|
+
}
|
|
1059
|
+
return this._sortColumns_cached;
|
|
1060
|
+
},
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* whether the column can be sorted or not
|
|
1064
|
+
* - if sortable passed as false, it will be false
|
|
1065
|
+
* - if baseColumn exists, it will return the column order of baseColumn
|
|
1066
|
+
* - otherwise it will be true
|
|
1067
|
+
* @type {boolean}
|
|
1068
|
+
*/
|
|
1069
|
+
get sortable() {
|
|
1070
|
+
if (this._sortable === undefined) {
|
|
1071
|
+
this._determineSortable();
|
|
1072
|
+
}
|
|
1073
|
+
return this._sortable;
|
|
1074
|
+
},
|
|
1075
|
+
|
|
1076
|
+
_determineSortable: function () {
|
|
1077
|
+
this._sortColumns_cached = [{column: decodeURIComponent(this.term), descending: false}];
|
|
1078
|
+
this._sortable = true;
|
|
1079
|
+
|
|
1080
|
+
if (!this.baseColumn) return;
|
|
1081
|
+
|
|
1082
|
+
var baseSortCols = this.baseColumn._getSortColumns(this._context);
|
|
1083
|
+
if (typeof baseSortCols === 'undefined') {
|
|
1084
|
+
this._sortColumns_cached = [];
|
|
1085
|
+
this._sortable = false;
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
this._sortColumns_cached = baseSortCols.map(function (ro) {
|
|
1090
|
+
return {"column": ro.column.name, "descending": ro.descending};
|
|
1091
|
+
});
|
|
1092
|
+
},
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* sets the context of column
|
|
1096
|
+
* @private
|
|
1097
|
+
*/
|
|
1098
|
+
_setContext: function (context) {
|
|
1099
|
+
this._context = isStringAndNotEmpty(context) ? context : '';
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
/**
|
|
1104
|
+
* Constructor for creating location object for creating a {@link ERMrest.AttributeGroupReference}
|
|
1105
|
+
|
|
1106
|
+
* @param {string} service the service part of url
|
|
1107
|
+
* @param {catalog} catalog the catalog object
|
|
1108
|
+
* @param {String} path the whole path string
|
|
1109
|
+
* @param {Object} searchObject search obect, it should have `term`, and `column`.
|
|
1110
|
+
* @param {Object[]} sortObject sort object, An array of objects with `column`, and `descending` as attribute.
|
|
1111
|
+
* @param {Object[]=} afterObject the object that will be used for paging to define after. It's an array of data
|
|
1112
|
+
* @param {Object[]=} beforeObject the object that will be used for paging to define before. It's an array of data
|
|
1113
|
+
* @constructor
|
|
1114
|
+
*/
|
|
1115
|
+
export function AttributeGroupLocation(service, catalog, path, searchObject, sortObject, afterObject, beforeObject) {
|
|
1116
|
+
/**
|
|
1117
|
+
* The uri to ermrest service
|
|
1118
|
+
* @type {string}
|
|
1119
|
+
*/
|
|
1120
|
+
this.service = service;
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* catalog object
|
|
1124
|
+
*
|
|
1125
|
+
* @type {Catalog}
|
|
1126
|
+
*/
|
|
1127
|
+
this.catalog = catalog;
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* The path that will be used for generating the uri in read.
|
|
1131
|
+
* @type {string}
|
|
1132
|
+
*/
|
|
1133
|
+
this.path = path;
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* The search object with "column" and "term".
|
|
1137
|
+
* @type {object}
|
|
1138
|
+
* @private
|
|
1139
|
+
*/
|
|
1140
|
+
this.searchObject = searchObject;
|
|
1141
|
+
|
|
1142
|
+
if (isObjectAndNotNull(this.searchObject)) {
|
|
1143
|
+
/**
|
|
1144
|
+
* The search term
|
|
1145
|
+
* @type {?string}
|
|
1146
|
+
*/
|
|
1147
|
+
this.searchTerm = this.searchObject.term;
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* The colum name that has been used for searching.
|
|
1151
|
+
* NOTE:
|
|
1152
|
+
* - we're going to encode this name. You don't have to encode it.
|
|
1153
|
+
* - Currently only search on one column, what about other columns?
|
|
1154
|
+
* - Maybe this should be private
|
|
1155
|
+
* @type {?string}
|
|
1156
|
+
*/
|
|
1157
|
+
this.searchColumn = this.searchObject.column;
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* The search filter string which can be used for creating the uri
|
|
1161
|
+
* @type {?string}
|
|
1162
|
+
*/
|
|
1163
|
+
this.searchFilter = _convertSearchTermToFilter(this.searchTerm, this.searchColumn, this.catalog);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* The sort object. It will be an array of object with the following format:
|
|
1168
|
+
* {"column": columnname, "descending": true|false}
|
|
1169
|
+
* @type {?Object[]}
|
|
1170
|
+
* @private
|
|
1171
|
+
*/
|
|
1172
|
+
this.sortObject = sortObject;
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Represents the paging. It will be an array of values.
|
|
1176
|
+
* v1, v2, v3.. are in the same order of columns in the sortObject
|
|
1177
|
+
* @type {?Object[]}
|
|
1178
|
+
* @private
|
|
1179
|
+
*/
|
|
1180
|
+
this.beforeObject = beforeObject;
|
|
1181
|
+
|
|
1182
|
+
if (isObjectAndNotNull(this.beforeObject)) {
|
|
1183
|
+
/**
|
|
1184
|
+
* The paging midifer string for creating the uri.
|
|
1185
|
+
* @type {?string}
|
|
1186
|
+
*/
|
|
1187
|
+
this.before = _getPagingModifier(this.beforeObject, true);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/**
|
|
1191
|
+
* Represents the paging. It will be an array of values.
|
|
1192
|
+
* v1, v2, v3.. are in the same order of columns in the sortObject
|
|
1193
|
+
* @type {?Object[]}
|
|
1194
|
+
* @private
|
|
1195
|
+
*/
|
|
1196
|
+
this.afterObject = afterObject;
|
|
1197
|
+
|
|
1198
|
+
if (isObjectAndNotNull(this.afterObject)) {
|
|
1199
|
+
/**
|
|
1200
|
+
* The paging midifer string for creating the uri.
|
|
1201
|
+
* @type {?string}
|
|
1202
|
+
*/
|
|
1203
|
+
this.after = _getPagingModifier(this.afterObject, false);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
if (this.after || this.before) {
|
|
1207
|
+
this.paging = (this.after ? this.after : "") + (this.before ? this.before : "");
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
}
|
|
1211
|
+
AttributeGroupLocation.prototype = {
|
|
1212
|
+
constructor: AttributeGroupLocation,
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* Given a searchObject, return a new location object.
|
|
1216
|
+
* @param {string} term
|
|
1217
|
+
* @return {ERMRest.AttributeGroupLocation}
|
|
1218
|
+
*/
|
|
1219
|
+
changeSearchTerm: function (term) {
|
|
1220
|
+
var searchObject = {"term": term, "column": this.searchColumn};
|
|
1221
|
+
return new AttributeGroupLocation(this.service, this.catalog, this.path, searchObject, this.sortObject, this.afterObject, this.beforeObject);
|
|
1222
|
+
},
|
|
1223
|
+
|
|
1224
|
+
/**
|
|
1225
|
+
* Given a sortObject, return a new location object.
|
|
1226
|
+
* This is removing the before and after (paging).
|
|
1227
|
+
* @param {object} searchObject
|
|
1228
|
+
* @return {ERMRest.AttributeGroupLocation}
|
|
1229
|
+
*/
|
|
1230
|
+
changeSort: function (sort) {
|
|
1231
|
+
return new AttributeGroupLocation(this.service, this.catalog, this.path, this.searchObject, sort);
|
|
1232
|
+
},
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* Given afterObject and beforeObject, return a new location object.
|
|
1236
|
+
* @param {object} afterObject
|
|
1237
|
+
* @param {object} beforeObject
|
|
1238
|
+
* @return {ERMRest.AttributeGroupLocation}
|
|
1239
|
+
*/
|
|
1240
|
+
changePage: function (afterObject, beforeObject) {
|
|
1241
|
+
return new AttributeGroupLocation(this.service, this.catalog, this.path, this.searchObject, this.sortObject, afterObject, beforeObject);
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* Can be used to access group aggregate functions.
|
|
1247
|
+
* Usage:
|
|
1248
|
+
* Clients _do not_ directly access this constructor. {@link ERMrest.AttributeGroupReference}
|
|
1249
|
+
* will access this constructor for purposes of fetching grouped aggregate data
|
|
1250
|
+
* for a specific column
|
|
1251
|
+
*
|
|
1252
|
+
* @param {AttributeGroupReference} reference The reference that this aggregate function belongs to
|
|
1253
|
+
* @memberof ERMrest
|
|
1254
|
+
* @constructor
|
|
1255
|
+
*/
|
|
1256
|
+
export function AttributeGroupReferenceAggregateFn (reference) {
|
|
1257
|
+
this._ref = reference;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
AttributeGroupReferenceAggregateFn.prototype = {
|
|
1261
|
+
/**
|
|
1262
|
+
* @type {Object}
|
|
1263
|
+
* @desc count aggregate representation
|
|
1264
|
+
* This does not count null values for the key since we're using `count distinct`.
|
|
1265
|
+
* Therefore the returned count might not be exactly the same as number of returned values.
|
|
1266
|
+
*/
|
|
1267
|
+
get countAgg() {
|
|
1268
|
+
if (this._ref.shortestKey.length > 1) {
|
|
1269
|
+
throw new Error("Cannot use count function, attribute group has more than one key column.");
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
return "cnt_d(" + this._ref.shortestKey[0].term + ")";
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* Constructs a Reference object based on {@link ERMrest.AttributeGroupReference}.
|
|
1278
|
+
*
|
|
1279
|
+
* This object will be the main object that client will interact with, when we want
|
|
1280
|
+
* to use ermrset `attributegroup` api with the bin aggregate. References are immutable
|
|
1281
|
+
* and therefore can be safely passed around and used between multiple client components
|
|
1282
|
+
* without risk that the underlying reference to server-side resources could change.
|
|
1283
|
+
*
|
|
1284
|
+
* Usage:
|
|
1285
|
+
* - Clients _do not_ directly access this constructor.
|
|
1286
|
+
* - This will currently be used by the aggregateGroup histogram function to return a
|
|
1287
|
+
* BucketAttributeGroupReference rather than a {@link ERMrest.Reference}
|
|
1288
|
+
*
|
|
1289
|
+
* @param {ReferenceColumn} baseColumn The column that is used for creating grouped aggregate
|
|
1290
|
+
* @param {Reference} baseRef The reference representing the column
|
|
1291
|
+
* @param {any} min The min value for the key column request
|
|
1292
|
+
* @param {any} max The max value for the key column request
|
|
1293
|
+
* @param {Integer} numberOfBuckets The number of buckets for the request
|
|
1294
|
+
* @param {any} bucketWidth the width of each bucket
|
|
1295
|
+
* @constructor
|
|
1296
|
+
*/
|
|
1297
|
+
export function BucketAttributeGroupReference(baseColumn, baseRef, min, max, numberOfBuckets, bucketWidth) {
|
|
1298
|
+
var location = new AttributeGroupLocation(baseRef.location.service, baseRef.table.schema.catalog, baseRef.location.ermrestCompactPath);
|
|
1299
|
+
var binTerm = "bin(" + fixedEncodeURIComponent(baseColumn.name) + ";" + numberOfBuckets + ";" + fixedEncodeURIComponent(min) + ";" + fixedEncodeURIComponent(max) + ")";
|
|
1300
|
+
|
|
1301
|
+
var keyColumns = [
|
|
1302
|
+
new AttributeGroupColumn("c1", binTerm, baseColumn, baseColumn.displayname, baseColumn.type, baseColumn.comment, true, true)
|
|
1303
|
+
];
|
|
1304
|
+
|
|
1305
|
+
var countName = "cnt(*)";
|
|
1306
|
+
|
|
1307
|
+
// if there's a join, we cannot use cnt(*) and we should count the shortestkeys of facet base table (not the projected table)
|
|
1308
|
+
if (baseRef.location.hasJoin) {
|
|
1309
|
+
countName = "cnt_d(" + baseRef.location.facetBaseTableAlias + ":" + fixedEncodeURIComponent(baseRef.facetBaseTable.shortestKey[0].name) + ")";
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
var aggregateColumns = [
|
|
1313
|
+
new AttributeGroupColumn("c2", countName, null, "Number of Occurrences", new Type({typename: "int"}), null, true, true)
|
|
1314
|
+
];
|
|
1315
|
+
|
|
1316
|
+
// call the parent constructor
|
|
1317
|
+
BucketAttributeGroupReference.superClass.call(this, keyColumns, aggregateColumns, location, baseRef.table.schema.catalog);
|
|
1318
|
+
|
|
1319
|
+
this._baseColumn = baseColumn;
|
|
1320
|
+
this._min = min;
|
|
1321
|
+
this._max = max;
|
|
1322
|
+
this._numberOfBuckets = numberOfBuckets;
|
|
1323
|
+
this._bucketWidth = bucketWidth;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// extend the prototype
|
|
1327
|
+
_extends(BucketAttributeGroupReference, AttributeGroupReference);
|
|
1328
|
+
|
|
1329
|
+
// properties to be overriden:
|
|
1330
|
+
BucketAttributeGroupReference.prototype.sort = function (sort) {
|
|
1331
|
+
verify(false, "Invalid function");
|
|
1332
|
+
};
|
|
1333
|
+
|
|
1334
|
+
BucketAttributeGroupReference.prototype.search = function (term) {
|
|
1335
|
+
verify(false, "Invalid function");
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
/**
|
|
1339
|
+
* Makes a request to the server to fetch the corresponding data for the given key
|
|
1340
|
+
* column and aggregate column. The returned data is then formatted for direct use
|
|
1341
|
+
* in the plotly APIs.
|
|
1342
|
+
*
|
|
1343
|
+
* The return object includes an array of x axis labels and another array of y axis
|
|
1344
|
+
* values used to represent the bars in the histogram. A third object is returned called
|
|
1345
|
+
* labels that includes an array of the min values for each bucket and another array
|
|
1346
|
+
* for the max values of each bucket. Together, labels.min[x] and labels.min[y],
|
|
1347
|
+
* represent the range for each bucket (bar in the histogram) at that particular index.
|
|
1348
|
+
*
|
|
1349
|
+
* @param {Object} contextHeaderParams the object that we want to log.
|
|
1350
|
+
* @return {Object} data object that contains 2 arrays and another object with 2 arrays
|
|
1351
|
+
*/
|
|
1352
|
+
BucketAttributeGroupReference.prototype.read = function (contextHeaderParams) {
|
|
1353
|
+
// uses the current known min value and adds the binWidth to it to generate the max label
|
|
1354
|
+
// which is then used as the next min label (because we didn't have a next min)
|
|
1355
|
+
function calculateWidthLabel(min, binWidth) {
|
|
1356
|
+
var nextLabel;
|
|
1357
|
+
if (currRef._keyColumns[0].type.rootName.indexOf("date") > -1) {
|
|
1358
|
+
nextLabel = moment(min).add(binWidth, 'd').format(_dataFormats.DATE);
|
|
1359
|
+
} else if (currRef._keyColumns[0].type.rootName.indexOf("timestamp") > -1) {
|
|
1360
|
+
nextLabel = moment(min).add(binWidth, 's').format(_dataFormats.DATETIME.return);
|
|
1361
|
+
} else {
|
|
1362
|
+
nextLabel = (min + binWidth);
|
|
1363
|
+
}
|
|
1364
|
+
return nextLabel;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
try {
|
|
1368
|
+
var defer = ConfigService.q.defer();
|
|
1369
|
+
|
|
1370
|
+
var uri = this.uri;
|
|
1371
|
+
|
|
1372
|
+
var currRef = this;
|
|
1373
|
+
if (!contextHeaderParams || !isObject(contextHeaderParams)) {
|
|
1374
|
+
contextHeaderParams = {"action": "read"};
|
|
1375
|
+
}
|
|
1376
|
+
var config = {
|
|
1377
|
+
headers: this._generateContextHeader(contextHeaderParams)
|
|
1378
|
+
};
|
|
1379
|
+
this._server.http.get(uri, config).then(function (response) {
|
|
1380
|
+
var data = {
|
|
1381
|
+
x: [],
|
|
1382
|
+
y: []
|
|
1383
|
+
};
|
|
1384
|
+
|
|
1385
|
+
var labels = {
|
|
1386
|
+
min: [],
|
|
1387
|
+
max: []
|
|
1388
|
+
};
|
|
1389
|
+
|
|
1390
|
+
var min, max;
|
|
1391
|
+
|
|
1392
|
+
/**
|
|
1393
|
+
* Loops through the returned response data and defines the values in x, y, labels.min, and labels.max
|
|
1394
|
+
* this only considers rows that are returned with values. Each row returned has a `c1` and `c2` value
|
|
1395
|
+
* - c1: an array with 3 values, [bucketIndex, min, max]
|
|
1396
|
+
* - c2: an integer representing the number of rows with a value between the min and max for that bucket index
|
|
1397
|
+
**/
|
|
1398
|
+
for (var i=0; i<response.data.length; i++) {
|
|
1399
|
+
var index = response.data[i].c1[0];
|
|
1400
|
+
if (index !== null) {
|
|
1401
|
+
min = response.data[i].c1[1];
|
|
1402
|
+
max = response.data[i].c1[2];
|
|
1403
|
+
|
|
1404
|
+
if (currRef._keyColumns[0].type.rootName.indexOf("date") > -1) {
|
|
1405
|
+
min = min !== null ? moment(min).format(_dataFormats.DATE) : null;
|
|
1406
|
+
max = max !== null ? moment(max).format(_dataFormats.DATE) : null;
|
|
1407
|
+
} else if (currRef._keyColumns[0].type.rootName.indexOf("timestamp") > -1) {
|
|
1408
|
+
min = min !== null ? moment(min).format(_dataFormats.DATETIME.return) : null;
|
|
1409
|
+
max = max !== null ? moment(max).format(_dataFormats.DATETIME.return) : null;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
labels.min[index] = min;
|
|
1413
|
+
labels.max[index] = max;
|
|
1414
|
+
|
|
1415
|
+
data.x[index] = min;
|
|
1416
|
+
data.y[index] = response.data[i].c2;
|
|
1417
|
+
}
|
|
1418
|
+
// else if null (this is the null bin)
|
|
1419
|
+
// we currently don't want to do anything with the null values
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// This should be set to the number of buckets to include the # of bins we want to display + the above max and below min bucket
|
|
1423
|
+
// loops through the data and generates the labels for rows that did not return with a value from the bin API
|
|
1424
|
+
for (var j=0; j<currRef._numberOfBuckets+2; j++) {
|
|
1425
|
+
// if no value is present (null is a value), we didn't get a bucket back for this index
|
|
1426
|
+
if (data.x[j] === undefined) {
|
|
1427
|
+
// determine x axis label
|
|
1428
|
+
|
|
1429
|
+
// figure out min first
|
|
1430
|
+
// no label for index 0
|
|
1431
|
+
if (j==0) {
|
|
1432
|
+
min = null;
|
|
1433
|
+
} else {
|
|
1434
|
+
min = labels.max[j-1];
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// use min to determine max
|
|
1438
|
+
// if there was a response row for next index, get min of next value
|
|
1439
|
+
if (labels.min[j+1]) {
|
|
1440
|
+
max = labels.min[j+1];
|
|
1441
|
+
} else {
|
|
1442
|
+
if (j == 0) {
|
|
1443
|
+
max = currRef._min;
|
|
1444
|
+
|
|
1445
|
+
if (currRef._keyColumns[0].type.rootName.indexOf("date") > -1) {
|
|
1446
|
+
max = moment(max).format(_dataFormats.DATE);
|
|
1447
|
+
} else if (currRef._keyColumns[0].type.rootName.indexOf("timestamp") > -1) {
|
|
1448
|
+
max = moment.utc(max).format(_dataFormats.DATETIME.return);
|
|
1449
|
+
}
|
|
1450
|
+
} else {
|
|
1451
|
+
max = calculateWidthLabel(min, currRef._bucketWidth);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
labels.min[j] = min;
|
|
1456
|
+
labels.max[j] = max;
|
|
1457
|
+
|
|
1458
|
+
data.x[j] = min;
|
|
1459
|
+
data.y[j] = 0;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// remove the first bin (null-min)
|
|
1464
|
+
data.x.splice(0, 1);
|
|
1465
|
+
data.y.splice(0, 1);
|
|
1466
|
+
labels.min.splice(0, 1);
|
|
1467
|
+
labels.max.splice(0, 1);
|
|
1468
|
+
|
|
1469
|
+
data.labels = labels;
|
|
1470
|
+
|
|
1471
|
+
defer.resolve(data);
|
|
1472
|
+
|
|
1473
|
+
}).catch(function (response) {
|
|
1474
|
+
var error = ErrorService.responseToError(response);
|
|
1475
|
+
defer.reject(error);
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
return defer.promise;
|
|
1479
|
+
|
|
1480
|
+
} catch (e) {
|
|
1481
|
+
return ConfigService.q.reject(e);
|
|
1482
|
+
}
|
|
1483
|
+
};
|