@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.
Files changed (71) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +55 -0
  3. package/dist/ermrest.d.ts +3481 -0
  4. package/dist/ermrest.js +45 -0
  5. package/dist/ermrest.js.gz +0 -0
  6. package/dist/ermrest.js.map +1 -0
  7. package/dist/ermrest.min.js +45 -0
  8. package/dist/ermrest.min.js.gz +0 -0
  9. package/dist/ermrest.min.js.map +1 -0
  10. package/dist/ermrest.ver.txt +1 -0
  11. package/dist/stats.html +4949 -0
  12. package/js/ag_reference.js +1483 -0
  13. package/js/core.js +4931 -0
  14. package/js/datapath.js +336 -0
  15. package/js/export.js +956 -0
  16. package/js/filters.js +192 -0
  17. package/js/format.js +344 -0
  18. package/js/hatrac.js +1130 -0
  19. package/js/json_ld_validator.js +285 -0
  20. package/js/parser.js +2320 -0
  21. package/js/setup/node.js +27 -0
  22. package/js/utils/helpers.js +2300 -0
  23. package/js/utils/json_ld_schema.js +680 -0
  24. package/js/utils/pseudocolumn_helpers.js +2196 -0
  25. package/package.json +79 -0
  26. package/src/index.ts +204 -0
  27. package/src/models/comment.ts +14 -0
  28. package/src/models/deferred-promise.ts +16 -0
  29. package/src/models/display-name.ts +5 -0
  30. package/src/models/errors.ts +408 -0
  31. package/src/models/path-prefix-alias-mapping.ts +130 -0
  32. package/src/models/reference/bulk-create-foreign-key-object.ts +133 -0
  33. package/src/models/reference/citation.ts +98 -0
  34. package/src/models/reference/contextualize.ts +535 -0
  35. package/src/models/reference/google-dataset-metadata.ts +72 -0
  36. package/src/models/reference/index.ts +14 -0
  37. package/src/models/reference/page.ts +520 -0
  38. package/src/models/reference/reference-aggregate-fn.ts +37 -0
  39. package/src/models/reference/reference.ts +2813 -0
  40. package/src/models/reference/related-reference.ts +467 -0
  41. package/src/models/reference/tuple.ts +652 -0
  42. package/src/models/reference-column/asset-pseudo-column.ts +498 -0
  43. package/src/models/reference-column/column-aggregate.ts +313 -0
  44. package/src/models/reference-column/facet-column.ts +1380 -0
  45. package/src/models/reference-column/foreign-key-pseudo-column.ts +626 -0
  46. package/src/models/reference-column/inbound-foreign-key-pseudo-column.ts +131 -0
  47. package/src/models/reference-column/index.ts +13 -0
  48. package/src/models/reference-column/key-pseudo-column.ts +236 -0
  49. package/src/models/reference-column/pseudo-column.ts +850 -0
  50. package/src/models/reference-column/reference-column.ts +740 -0
  51. package/src/models/source-object-node.ts +156 -0
  52. package/src/models/source-object-wrapper.ts +694 -0
  53. package/src/models/table-source-definitions.ts +98 -0
  54. package/src/services/authn.ts +43 -0
  55. package/src/services/catalog.ts +37 -0
  56. package/src/services/config.ts +202 -0
  57. package/src/services/error.ts +247 -0
  58. package/src/services/handlebars.ts +607 -0
  59. package/src/services/history.ts +136 -0
  60. package/src/services/http.ts +536 -0
  61. package/src/services/logger.ts +70 -0
  62. package/src/services/mustache.ts +0 -0
  63. package/src/utils/column-utils.ts +308 -0
  64. package/src/utils/constants.ts +526 -0
  65. package/src/utils/markdown-utils.ts +855 -0
  66. package/src/utils/reference-utils.ts +1658 -0
  67. package/src/utils/template-utils.ts +0 -0
  68. package/src/utils/type-utils.ts +89 -0
  69. package/src/utils/value-utils.ts +127 -0
  70. package/tsconfig.json +30 -0
  71. 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
+ };