@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
package/js/parser.js ADDED
@@ -0,0 +1,2320 @@
1
+ /* eslint-disable prettier/prettier */
2
+ /* eslint-disable @typescript-eslint/no-unused-vars */
3
+ /* eslint-disable @typescript-eslint/no-this-alias */
4
+ /* eslint-disable no-useless-escape */
5
+ // models
6
+ import {
7
+ MalformedURIError,
8
+ InvalidCustomFacetOperatorError,
9
+ InvalidInputError,
10
+ InvalidPageCriteria,
11
+ InvalidFacetOperatorError,
12
+ InvalidFilterOperatorError,
13
+ } from '@isrd-isi-edu/ermrestjs/src/models/errors';
14
+
15
+ // services
16
+ import HistoryService from '@isrd-isi-edu/ermrestjs/src/services/history';
17
+
18
+ // utils
19
+ import {
20
+ _ERMrestFeatures,
21
+ _ERMrestFilterPredicates,
22
+ _facetFilterTypes,
23
+ _facetFilterTypeNames,
24
+ FILTER_TYPES,
25
+ _systemColumnNames,
26
+ } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
27
+ import { isObjectAndNotNull, isStringAndNotEmpty, verify } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
28
+ import { fixedEncodeURIComponent, simpleDeepCopy, stripTrailingSlash, trimSlashes } from '@isrd-isi-edu/ermrestjs/src/utils/value-utils';
29
+ import { _facetingErrors, _FacetsLogicalOperators, _specialSourceDefinitions, _parserAliases } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
30
+
31
+ // legacy
32
+ import { decodeFacet, encodeFacet, _encodeRegexp } from '@isrd-isi-edu/ermrestjs/js/utils/helpers';
33
+ import { _renderFacet, _sourceColumnHelpers } from '@isrd-isi-edu/ermrestjs/js/utils/pseudocolumn_helpers';
34
+ import PathPrefixAliasMapping from '@isrd-isi-edu/ermrestjs/src/models/path-prefix-alias-mapping';
35
+
36
+ /**
37
+ * The ERMrest service name. Internal use only.
38
+ * @type {string}
39
+ * @private
40
+ */
41
+ var _service_name = 'ermrest';
42
+
43
+ /**
44
+ * This function parses a URI and constructs a representation of the URI.
45
+ * @memberof ERMrest
46
+ * @function parse
47
+ * @param {String} uri An ERMrest resource URI to be parsed.
48
+ * @param {Catalog=} catalogObject the catalog object that the uri is based on
49
+ * @returns {Location} Location object created from the URI.
50
+ * @throws {InvalidInputError} If the URI does not contain the
51
+ * service name.
52
+ */
53
+ export const parse = function (uri, catalogObject) {
54
+ var svc_idx = uri.indexOf(_service_name);
55
+ if (svc_idx < 0) {
56
+ throw new InvalidInputError('uri not contain the expected service name: ' + _service_name);
57
+ }
58
+
59
+ return new Location(uri, catalogObject);
60
+
61
+ };
62
+
63
+ /**
64
+ * Create the proper path to search
65
+ * NOTE If searchColumnNames is passed, we will not use the built-in search
66
+ * feature anymore and will fallback to raw ermrest regular expression search.
67
+ * Therefore it's highly recommended that you don't use this parameter and instead
68
+ * modify the search-box source definition of the table.
69
+ *
70
+ * @param {string} schemaName Name of schema, can be null
71
+ * @param {string} tableName Name of table
72
+ * @param {string} searchTerm the search keyword
73
+ * @param {string[]?} searchColumNames the name of columns that should be used for search
74
+ * @return {string} a path that ERMrestJS understands and can parse, can be undefined
75
+ */
76
+ export const createSearchPath = function (catalogId, schemaName, tableName, searchTerm, searchColumNames) {
77
+ verify(typeof catalogId === "string" && catalogId.length > 0, "catalogId must be a string.");
78
+ verify(typeof tableName === "string" && tableName.length > 0, "tableName must be a string.");
79
+
80
+ var hasSearch = (typeof searchTerm === "string" && searchTerm.trim().length > 0);
81
+ var encode = fixedEncodeURIComponent;
82
+
83
+ if (hasSearch && Array.isArray(searchColumNames) && searchColumNames.length > 0) {
84
+ var compactPath = "#" + catalogId + "/";
85
+ var parsedSearch = [];
86
+
87
+ if (schemaName) {
88
+ compactPath += encode(schemaName) + ":";
89
+ }
90
+ compactPath += encode(tableName);
91
+
92
+ searchColumNames.forEach(function (col) {
93
+ parsedSearch.push(encode(col) + "::ciregexp::" + encode(searchTerm.trim()));
94
+ });
95
+
96
+ return compactPath + "/" + parsedSearch.join(";");
97
+ }
98
+
99
+ // create the facet object so we can pass it to ermrestjs
100
+ var facets;
101
+ if (hasSearch) {
102
+ facets = {
103
+ "and": [{ "sourcekey": "search-box", "search": [searchTerm.trim()] }]
104
+ };
105
+ }
106
+
107
+ return createPath(catalogId, schemaName, tableName, facets);
108
+
109
+ };
110
+
111
+ /**
112
+ * Given tableName, schemaName, and facets will generate a path in the following format:
113
+ * #<catalogId>/<tableName>:<schemaName>/*::facets::<FACETSBLOB>/*::cfacets:<CUSTOMFACETBLOB>/
114
+ * @param {string} catalogId the id of catalog
115
+ * @param {string} schemaName Name of schema, can be null
116
+ * @param {string} tableName Name of table
117
+ * @param {object} facets an object
118
+ * @param {object} cfacets an object
119
+ * @return {string} a path that ERMrestJS understands and can parse, can be undefined
120
+ */
121
+ export const createPath = function (catalogId, schemaName, tableName, facets, cfacets) {
122
+ verify(typeof catalogId === "string" && catalogId.length > 0, "catalogId must be a string.");
123
+ verify(typeof tableName === "string" && tableName.length > 0, "tableName must be a string.");
124
+
125
+ var compactPath = "#" + catalogId + "/";
126
+ if (schemaName) {
127
+ compactPath += fixedEncodeURIComponent(schemaName) + ":";
128
+ }
129
+ compactPath += fixedEncodeURIComponent(tableName);
130
+
131
+ if (facets && typeof facets === "object" && Object.keys(facets).length !== 0) {
132
+ compactPath += "/*::facets::" + encodeFacet(facets);
133
+ }
134
+
135
+ if (cfacets && typeof cfacets === "object" && Object.keys(cfacets).length !== 0) {
136
+ compactPath += "/*::cfacets::" + encodeFacet(cfacets);
137
+ }
138
+
139
+ return compactPath;
140
+ };
141
+
142
+
143
+ /**
144
+ * Given tableName, schemaName, and facets will return a location object
145
+ * @param {string} service the service url
146
+ * @param {string} schemaName Name of schema, can be null
147
+ * @param {string} tableName Name of table
148
+ * @param {object} facets an object
149
+ * @param {object} cfacets an object
150
+ * @param {Catalog} [catalogObject] the catalog object (optional)
151
+ * @return {string} a path that ERMrestJS understands and can parse, can be undefined
152
+ */
153
+ export const createLocation = function (service, catalogId, schemaName, tableName, facets, cfacets, catalogObject) {
154
+ verify(typeof service === "string" && service.length > 0, "service must be a string.");
155
+ verify(typeof catalogId === "string" && catalogId.length > 0, "catalogId must be a string.");
156
+ verify(typeof tableName === "string" && tableName.length > 0, "tableName must be a string.");
157
+
158
+ var compactPath = "";
159
+ if (schemaName) {
160
+ compactPath += fixedEncodeURIComponent(schemaName) + ":";
161
+ }
162
+ compactPath += fixedEncodeURIComponent(tableName);
163
+
164
+ if (facets && typeof facets === "object" && Object.keys(facets).length !== 0) {
165
+ compactPath += "/*::facets::" + encodeFacet(facets);
166
+ }
167
+
168
+ if (cfacets && typeof cfacets === "object" && Object.keys(cfacets).length !== 0) {
169
+ compactPath += "/*::cfacets::" + encodeFacet(cfacets);
170
+ }
171
+
172
+ if (service.endsWith("/")) service = service.slice(0, -1);
173
+ return parse(service + "/catalog/" + catalogId + "/entity/" + compactPath, catalogObject);
174
+ };
175
+
176
+ /**
177
+ * The parse handles URI in this format
178
+ * <service>/catalog/<catalog_id>/<api>/<schema>:<table>/[path parts][modifiers][query params]
179
+ *
180
+ * where
181
+ * - path part: [facet][cfacet][filter]
182
+ * - modifiers: [@sort(col...)][@before(...)/@after(...)]
183
+ *
184
+ *
185
+ * uri = <service>/catalog/<catalog>/<api>/<path><sort><paging>?<limit>
186
+ * service: the ERMrest service endpoint such as https://www.example.com/ermrest.
187
+ * catalog: the catalog identifier for one dataset.
188
+ * api: the API or data resource space identifier such as entity, attribute, attributegroup, or aggregate.
189
+ * path: the data path which identifies one filtered entity set with optional joined context.
190
+ * sort: optional @sort
191
+ * paging: optional @before/@after
192
+ * query params: optional ?
193
+ *
194
+ *
195
+ * NOTE: For parsing the facet, Location object needs the catalog object.
196
+ * it should either be passed while creating the location object, or set after.
197
+ * @param {String} uri full path
198
+ * @param {Catalog} the catalog object for parsing some parts of url might be needed.
199
+ * @constructor
200
+ */
201
+ export function Location(uri, catalogObject) {
202
+
203
+ // full uri
204
+ this._uri = uri;
205
+
206
+ // extract the query params
207
+ if (uri.indexOf("?") !== -1) {
208
+ parts = uri.split("?");
209
+ uri = parts[0];
210
+ this._queryParamsString = parts[1];
211
+ this._queryParams = _getQueryParams(parts[1]);
212
+ } else {
213
+ this._queryParams = {};
214
+ }
215
+
216
+ // compactUri is the full uri without modifiers
217
+ if (uri.indexOf("@sort(") !== -1) {
218
+ this._compactUri = uri.split("@sort(")[0];
219
+ } else if (uri.indexOf("@before(") !== -1) {
220
+ this._compactUri = uri.split("@before(")[0];
221
+ } else if (uri.indexOf("@after(") !== -1) {
222
+ this._compactUri = uri.split("@after(")[0];
223
+ } else {
224
+ this._compactUri = uri;
225
+ }
226
+ // there might be an extra slash at the end of the url
227
+ this._compactUri = stripTrailingSlash(this._compactUri);
228
+
229
+ // service
230
+ parts = uri.match(/(.*)\/catalog\/([^\/]*)\/(entity|attribute|aggregate|attributegroup)\/(.*)/);
231
+ this._service = parts[1];
232
+
233
+ // catalog id
234
+ this._catalogSnapshot = parts[2];
235
+
236
+ var catalogParts = this._catalogSnapshot.split('@');
237
+ this._catalog = catalogParts[0];
238
+ this._version = catalogParts[1] || null;
239
+
240
+ if (catalogObject) {
241
+ if (catalogObject.id !== this._catalogSnapshot) {
242
+ throw new InvalidInputError("Given catalog object is not the same catalog used in the url.");
243
+ }
244
+
245
+ this._catalogObject = catalogObject;
246
+ }
247
+
248
+ // api
249
+ this._api = parts[3];
250
+
251
+ // path is everything after catalog id
252
+ this._path = parts[4];
253
+
254
+ var modifiers = uri.split(this._compactUri)[1]; // emtpy string if no modifiers
255
+
256
+ // compactPath is path without modifiers
257
+ this._compactPath = (modifiers === "" ? this._path : this._path.split(modifiers)[0]);
258
+
259
+ // there might be an extra slash at the end of the url
260
+ this._compactPath = stripTrailingSlash(this._compactPath);
261
+
262
+ // <sort>/<page>
263
+ // sort and paging
264
+ if (modifiers) {
265
+ if (modifiers.indexOf("@sort(") !== -1) {
266
+ this._sort = modifiers.match(/(@sort\([^\)]*\))/)[1];
267
+ }
268
+ // sort must specified to use @before and @after
269
+ if (modifiers.indexOf("@before(") !== -1) {
270
+ if (this._sort) {
271
+ this._before = modifiers.match(/(@before\([^\)]*\))/)[1];
272
+ } else {
273
+ throw new InvalidPageCriteria("Sort modifier is required with paging.", this._path);
274
+ }
275
+ }
276
+
277
+ if (modifiers.indexOf("@after(") !== -1) {
278
+ if (this._sort) {
279
+ this._after = modifiers.match(/(@after\([^\)]*\))/)[1];
280
+ } else {
281
+ throw new InvalidPageCriteria("Sort modifier is required with paging.", this._path);
282
+ }
283
+ }
284
+ }
285
+
286
+ // Split compact path on '/'
287
+ var parts = this._compactPath.split('/');
288
+
289
+ if (parts.length === 0) throw new MalformedURIError("Given url must start with `schema:table.");
290
+
291
+ //<schema:table>
292
+ // first schema name and first table name
293
+ var params = parts[0].split(':');
294
+ if (params.length > 1) {
295
+ this._rootSchemaName = decodeURIComponent(params[0]);
296
+ this._rootTableName = decodeURIComponent(params[1]);
297
+ } else {
298
+ this._rootSchemaName = "";
299
+ this._rootTableName = decodeURIComponent(params[0]);
300
+ }
301
+
302
+ // pathParts: <joins/facet/cfacet/filter/>
303
+ var joinRegExp = /(?:left|right|full|^)\((.*)\)=\((.*:.*:.*)\)/,
304
+ facetsRegExp = /\*::facets::(.+)/,
305
+ customFacetsRegExp = /\*::cfacets::(.+)/;
306
+
307
+ var table = this._rootTableName, schema = this._rootSchemaName;
308
+ var self = this, pathParts = [], alias, match, prevJoin = false, startWithT1 = false, aliasNumber;
309
+ var facets, cfacets, filter, filtersString, join, joins = [];
310
+
311
+ // go through each of the parts
312
+ parts.forEach(function (part, index) {
313
+ // this is the schema:table
314
+ if (index === 0) return;
315
+
316
+ // slash at the end of the url
317
+ if (index === parts.length - 1 && part === "") return;
318
+
319
+ // join
320
+ match = part.match(joinRegExp);
321
+ if (match) {
322
+ // there wasn't any join before, so this is the start of new path,
323
+ // so we should create an object for the previous one.
324
+ if (!prevJoin && index !== 1) {
325
+ /**
326
+ * we're creating this alias for the previous section, so we should use the previous index
327
+ * Alias will be T, T1, T2, ... (notice we don't have T0)
328
+ */
329
+ aliasNumber = pathParts.length;
330
+
331
+ /**
332
+ * T is always preserved for the initial schema:table. if the first pathPart doesn't have any joins
333
+ * and has facet/filters, then it's for the schema:table and so we should start with T.
334
+ * but if has join, then it means that the schema:table didn't have any filter/facets. so we have to start from T1,
335
+ * and the rest of the parts should just add one more
336
+ */
337
+ if (pathParts.length === 0 && joins.length > 0) {
338
+ startWithT1 = true;
339
+ }
340
+ if (startWithT1) aliasNumber++;
341
+ alias = _parserAliases.JOIN_TABLE_PREFIX + (aliasNumber > 0 ? aliasNumber : "");
342
+ pathParts.push(new PathPart(alias, joins, schema, table, facets, cfacets, filter, filtersString));
343
+ filter = undefined; filtersString = undefined; cfacets = undefined; facets = undefined; join = undefined; joins = [];
344
+ }
345
+
346
+ join = _createParsedJoinFromStr(match, table, schema);
347
+ joins.push(join);
348
+ prevJoin = true;
349
+
350
+ // will be used for the next join
351
+ table = joins[joins.length-1].toTable;
352
+ schema = joins[joins.length-1].toSchema;
353
+ return;
354
+ }
355
+
356
+ prevJoin = false;
357
+
358
+ // facet
359
+ match = part.match(facetsRegExp);
360
+ if (match) {
361
+ if (facets) {
362
+ throw new InvalidFacetOperatorError(self._path, _facetingErrors.duplicateFacets);
363
+ }
364
+ facets = new ParsedFacets(match[1], self._path);
365
+ return;
366
+ }
367
+
368
+ // custom facet
369
+ match = part.match(customFacetsRegExp);
370
+ if (match) {
371
+ if (cfacets) {
372
+ throw new InvalidCustomFacetOperatorError(self._path, _facetingErrors.duplicateFacets);
373
+ }
374
+ cfacets = new CustomFacets(match[1], self._path);
375
+ return;
376
+ }
377
+
378
+ // filter
379
+ filtersString = part;
380
+ filter = _processFilterPathPart(part, self._path);
381
+ });
382
+
383
+ // this is for the last part of url that might not end with join.
384
+ if (filter || cfacets || facets || joins.length > 0) {
385
+ pathParts.push(new PathPart(_parserAliases.MAIN_TABLE, joins, schema, table, facets, cfacets, filter, filtersString));
386
+ }
387
+
388
+ this._pathParts = pathParts;
389
+ }
390
+
391
+ Location.prototype = {
392
+
393
+ /**
394
+ * Override the toString function
395
+ * @returns {String} the string representation of the Location, which is the full URI.
396
+ */
397
+ toString: function(){
398
+ return this.uri;
399
+ },
400
+
401
+
402
+ /**
403
+ * The complete uri that is understandable by ermrestjs
404
+ * NOTE: some of the components might not be understanable by ermrest, because of pseudo operator (e.g., ::facets::).
405
+ *
406
+ * @returns {String} The full URI of the location
407
+ */
408
+ get uri() {
409
+ if (this._uri === undefined) {
410
+ this._uri = this.compactUri + this._modifiers + (this.queryParamsString ? "?" + this.queryParamsString : "");
411
+ }
412
+ return this._uri;
413
+ },
414
+
415
+ /**
416
+ * <service>/catalog/<catalogId>/<api>/<rootSchema:rootTable>/<filters>/<joins>/<search>
417
+ *
418
+ * NOTE: some of the components might not be understanable by ermrest, because of pseudo operator (e.g., ::search::).
419
+ * @returns {String} The URI without modifiers or queries
420
+ */
421
+ get compactUri() {
422
+ if (this._compactUri === undefined) {
423
+ var path = [this.service, "catalog", this.catalog, this.api].join("/");
424
+ this._compactUri = path + "/" + this.compactPath;
425
+ }
426
+ return this._compactUri;
427
+ },
428
+
429
+ /**
430
+ * A path that is understanable by ermrestjs. It includes the modifiers.
431
+ * NOTE: some of the components might not be understanable by ermrest, because of pseudo operator (e.g., ::search::).
432
+ *
433
+ * @returns {String} Path portion of the URI
434
+ * This is everything after the catalog id
435
+ */
436
+ get path() {
437
+ if (this._path === undefined) {
438
+ this._path = this.compactPath + this._modifiers;
439
+ }
440
+ return this._path;
441
+ },
442
+
443
+ /**
444
+ * A path that is understanable by ermrestjs. It doesn't inlcude the modifiers
445
+ * NOTE: some of the components might not be understanable by ermrest, because of pseudo operator (e.g., ::facets::).
446
+ *
447
+ * @returns {String} Path without modifiers or queries
448
+ */
449
+ get compactPath() {
450
+ if (this._compactPath === undefined) {
451
+ var self = this;
452
+ var uri = "";
453
+ if (this.rootSchemaName) {
454
+ uri += fixedEncodeURIComponent(this.rootSchemaName) + ":";
455
+ }
456
+ uri += fixedEncodeURIComponent(this.rootTableName);
457
+
458
+ uri += this.pathParts.reduce(function (prev, part, i) {
459
+ var res = prev;
460
+ if (part.joins) {
461
+ res += part.joins.reduce(function (prev, join, i) {
462
+ return prev + "/" + join.str;
463
+ }, "");
464
+ }
465
+ if (part.facets) {
466
+ res += "/*::facets::" + part.facets.encoded;
467
+ }
468
+ if (part.customFacets) {
469
+ res += "/*::cfacets::" + part.customFacets.encoded;
470
+ }
471
+ if (part.filtersString) {
472
+ res += "/" + part.filtersString;
473
+ }
474
+ return res;
475
+ }, "");
476
+
477
+ this._compactPath = uri;
478
+ }
479
+ return this._compactPath;
480
+ },
481
+
482
+ /**
483
+ * Returns a uri that ermrest understands.
484
+ * should only be used for internal usage and sending request to ermrest
485
+ *
486
+ * @returns {String} The URI without modifiers or queries for ermrest
487
+ */
488
+ get ermrestCompactUri() {
489
+ if (this._ermrestCompactUri === undefined) {
490
+ var path = [this.service, "catalog", this.catalog, this.api].join("/");
491
+ this._ermrestCompactUri = path + "/" + this.ermrestCompactPath;
492
+ }
493
+ return this._ermrestCompactUri;
494
+ },
495
+
496
+ /**
497
+ * Returns a path that ermrest understands.
498
+ * should only be used for internal usage and sending request to ermrest.
499
+ *
500
+ * @returns {String} Path portion of the URI
501
+ * This is everything after the catalog id for ermrest
502
+ */
503
+ get ermrestPath() {
504
+ if (this._ermrestPath === undefined) {
505
+ this._ermrestPath = this.ermrestCompactPath + this._modifiers;
506
+ }
507
+ return this._ermrestPath;
508
+ },
509
+
510
+ /**
511
+ * Returns a path that ermrest understands. It doesn't include the modifiers.
512
+ * This attribute will add extra aliases to the url, so the facets can refer to those
513
+ * aliases. So assuming that the follwoing is the path (F means the combination of filter, facet, and cfacet):
514
+ *
515
+ * S:R /F0 /J1 /F1 /J2 /F2 /…/Ji-1 /Fi-1 /Ji /Fi /Ji+1 /Fi+1 /… /Jn /Fn
516
+ *
517
+ * This will be the ermrest path:
518
+ *
519
+ * T:=S:R /F0 /T1:=J1 /F1 /… /Ti-1:=Ji-1 /Fi-1 /Ti:=Ji /Fi /Ti+1:=Ji+1 /Fi+1 /…./M:=Jn /Fn
520
+ *
521
+ * And if Fi has null filter, and therefore is using a right join,
522
+ * the following will be the ermest path:
523
+ *
524
+ * Ti:=Fi /Ti-1:=RevJi /Fi-1 /Ti-2:=RevJi-1 /… /T1:=RevJ2 /F1 /T:=RevJ1 /F0 /$Ti /Ti+1:=Ji+1 /Fi+1 /… /M:=Jn/Fn
525
+ *
526
+ * Which will be in this format:
527
+ * (<parsed facets starting from the facet with null>/<rev join of the parsed facets>)+/$T(facet with null)/(<parsed facet and join of parts after the null index>)*
528
+ *
529
+ * @param {Array} usedSourceObjects (optional) the source objects that are used in other parts of url (passed for path prefix logic)
530
+ * @returns {Object} an object wit the following properties:
531
+ * - `path`: Path without modifiers or queries for ermrest
532
+ * - `pathPrefixAliasMapping`: alias mapping that are used in the url
533
+ * - `isUsingRightJoin`: whether we've used right outer join for this path.
534
+ */
535
+ computeERMrestCompactPath: function (usedSourceObjects) {
536
+ var self = this;
537
+ var rightJoinIndex = -1, i;
538
+ var uri = "", alias, temp;
539
+
540
+ var getPathPartAliasMapping = function (index) {
541
+ if (index !== -1 && parsedPartsWithoutJoin[index].pathPrefixAliasMapping) {
542
+ return parsedPartsWithoutJoin[index].pathPrefixAliasMapping;
543
+ }
544
+ return new PathPrefixAliasMapping();
545
+ };
546
+
547
+ /**
548
+ * As part of `jonsStr`, if the table instance is already defined and added
549
+ * to the aliases, we might not add any new instances and just refer to the
550
+ * previously defined instance. In this case, the `jonsStr` function will
551
+ * change the alias property of pathPart to properly refer to the table instance.
552
+ *
553
+ * But the alias names used for the last pathPart is specific and should not be
554
+ * changed. Therefore we're making sure if there's a sourcekey that is supposed
555
+ * to use a shared instance, is using the forced alias name.
556
+ *
557
+ * Then the logic for generating new alias names will first look at the forced
558
+ * aliases and if the sourcekey is part of forced aliases will not generate a new
559
+ * one for it.
560
+ * @param {*} index
561
+ * @returns an object that represent forcedAliases
562
+ * @ignore
563
+ */
564
+ var getForcedAliases = function (index) {
565
+ var res = {};
566
+
567
+ // if last: the given index, otherwise: the one after
568
+ var part = self.pathParts[Math.min(index+1, self.pathParts.length - 1)];
569
+
570
+ if (part.joins.length > 0 && part.joins[0].sourceObjectWrapper) {
571
+ var sourceObject = part.joins[0].sourceObjectWrapper.sourceObject;
572
+ // make sure the alias that we set for the join is also used for share path
573
+ if (isStringAndNotEmpty(sourceObject.sourcekey)) {
574
+ res[sourceObject.sourcekey] = part.alias;
575
+ }
576
+ }
577
+ return res;
578
+ };
579
+
580
+ // returns the proper string presentation of a series of joins
581
+ // parameters:
582
+ // - joins: array of ParsedJoin objects.
583
+ // - alias: the alias that will be attached to the end of the path
584
+ // - reverse: whether we want to return the reversed join string
585
+ var joinsStr = function (part, alias, index, reverse) {
586
+ if (part.joins[0].sourceObjectWrapper) {
587
+
588
+ // when reversed, we're not using shared paths
589
+ if (reverse) {
590
+ return part.joins[0].sourceObjectWrapper.toString(true, false, alias);
591
+ }
592
+
593
+ // this join should be using the alias mapping of the previous facet
594
+ var wrapper = part.joins[0].sourceObjectWrapper;
595
+ temp = _sourceColumnHelpers.parseSourceNodesWithAliasMapping(
596
+ wrapper.sourceObjectNodes,
597
+ wrapper.lastForeignKeyNode,
598
+ wrapper.foreignKeyPathLength,
599
+ wrapper.sourceObject && isStringAndNotEmpty(wrapper.sourceObject.sourcekey) ? wrapper.sourceObject.sourcekey : null,
600
+ getPathPartAliasMapping(index -1), // share with the previous path part (facet)
601
+ index > 0 ? self.pathParts[index-1].alias : self.mainTableAlias, // the alias of previous part??
602
+ false,
603
+ alias
604
+ );
605
+
606
+ // make sure alias is updated based on what's used
607
+ // so while parsing the facets we're using the correct alias
608
+ part.alias = temp.usedOutAlias;
609
+ return temp.path;
610
+ } else {
611
+ var fn = reverse ? "reduceRight" : "reduce";
612
+ var joinStr = reverse ? "strReverse" : "str";
613
+ var last = reverse ? 0 : part.joins.length - 1;
614
+ var first = reverse ? part.joins.length -1 : 0;
615
+
616
+ return part.joins[fn](function (res, join, i) {
617
+ return res + (i !== first ? "/" : "") + ((i === last) ? (alias + ":=") : "") + join[joinStr];
618
+ }, "");
619
+ }
620
+ };
621
+
622
+ var projectedTable = null;
623
+ try {
624
+ projectedTable = self.catalogObject.schemas.findTable(self.tableName, self.schemaName);
625
+ } catch(exp) {
626
+ // fail silently
627
+ }
628
+ /**
629
+ * make sure the lastPathPartAliasMapping is defined
630
+ * NOTE: if the location doesn't have any facets, this object will
631
+ * be used. otherwise we will use the one generated based on facets
632
+ */
633
+ var lastPathPartAliasMapping = new PathPrefixAliasMapping(
634
+ self.pathParts.length > 0 ? getForcedAliases(self.pathParts.length-1) : null,
635
+ usedSourceObjects,
636
+ projectedTable
637
+ );
638
+
639
+ // get the parsed one, and count the number of right joins
640
+ // NOTE: we have to do this at first to find the facets with null
641
+ var parsedPartsWithoutJoin = self.pathParts.map(function (part, index) {
642
+ var res = [], facetRes;
643
+ var usedRef;
644
+ if (index == self.pathParts.length -1) {
645
+ // the Location.referenceObject is based on last part of url
646
+ usedRef= self.referenceObject;
647
+ }
648
+ // facet
649
+ if (part.facets) {
650
+ var currUsedSourceObjects = null;
651
+ var forcedAliases = getForcedAliases(index);
652
+ if (index == self.pathParts.length -1) {
653
+ currUsedSourceObjects = usedSourceObjects;
654
+ }
655
+ // the next join might have a sourceObjectWrapper that
656
+ // should be taken into account
657
+ else if (self.pathParts[index+1].joins.length > 0 && self.pathParts[index+1].joins[0].sourceObjectWrapper) {
658
+ currUsedSourceObjects = [self.pathParts[index+1].joins[0].sourceObjectWrapper.sourceObject];
659
+ }
660
+
661
+ facetRes = _renderFacet(
662
+ part.facets.decoded, part.alias, part.schema, part.table, self.catalog,
663
+ self.catalogObject, usedRef,
664
+ currUsedSourceObjects, forcedAliases
665
+ );
666
+ if (!facetRes.successful) {
667
+ throw new InvalidFacetOperatorError(self.path, facetRes.message);
668
+ }
669
+ if (facetRes.rightJoin) {
670
+ // we only allow one right join (null fitler)
671
+ if (rightJoinIndex !== -1) {
672
+ throw new MalformedURIError("Only one facet in url can have `null` filter.");
673
+ }
674
+ rightJoinIndex = index;
675
+ }
676
+ if (index == self.pathParts.length -1 ) {
677
+ lastPathPartAliasMapping = facetRes.pathPrefixAliasMapping;
678
+ }
679
+
680
+ res.push(facetRes.parsed);
681
+ }
682
+
683
+ // cfacet
684
+ if (part.customFacets) {
685
+ // TODO customFacets is not properly sharing prefix path
686
+ // and should be changed
687
+ // it requires preprocessing the usedSourcekeys based on both
688
+ // facets and customFacets
689
+ if (part.customFacets.facets) {
690
+ facetRes = _renderFacet(
691
+ part.customFacets.facets.decoded, part.alias, part.schema, part.table, self.catalog,
692
+ self.catalogObject, usedRef,
693
+ null, null
694
+ );
695
+ if (!facetRes.successful) {
696
+ throw new InvalidCustomFacetOperatorError(self.path, facetRes.message);
697
+ }
698
+ if (facetRes.rightJoin) {
699
+ throw new InvalidCustomFacetOperatorError(self.path, "`null` choice facet is not allowed in custom facets");
700
+ }
701
+ res.push(facetRes.parsed);
702
+ }
703
+
704
+ if (typeof part.customFacets.ermrestPath === "string") {
705
+ res.push(part.customFacets.ermrestPath);
706
+ }
707
+ }
708
+
709
+ //filter
710
+ if (part.filtersString) {
711
+ res.push(part.filtersString);
712
+ }
713
+
714
+ return {
715
+ parsed: res.join("/"),
716
+ pathPrefixAliasMapping: facetRes ? facetRes.pathPrefixAliasMapping : null
717
+ };
718
+ });
719
+
720
+ // no rightJoin: s:t/<parts>
721
+ if (rightJoinIndex === -1) {
722
+ uri = self.rootTableAlias + ":=";
723
+ if (self.rootSchemaName) {
724
+ uri += fixedEncodeURIComponent(self.rootSchemaName) + ":";
725
+ }
726
+ uri += fixedEncodeURIComponent(self.rootTableName);
727
+ }
728
+ // we have right index, then every path before null must be reversed
729
+ else {
730
+ // url format:
731
+ // (<parsed facets starting from the faect with null>/<rev join of the parsed facets>)+/$T(facet with null)/(<parsed facet and join of parts after the null index>)*
732
+ for (i = rightJoinIndex; i >= 0; i--) {
733
+ temp = parsedPartsWithoutJoin[i];
734
+ if (temp.parsed) {
735
+ uri += (uri.length > 0 ? "/" : "") + temp.parsed;
736
+ }
737
+
738
+ if (self.pathParts[i].joins.length > 0) {
739
+ // since we're reversing, we have to make sure we're using the
740
+ // alias of the previous pathpart
741
+ alias = i > 0 ? self.pathParts[i-1].alias : self.rootTableAlias;
742
+ uri += "/" + joinsStr(self.pathParts[i], alias, i, true);
743
+ }
744
+ }
745
+
746
+ // if there was pathParts before facet with null, change back to the facet with null
747
+ if (self.pathParts[rightJoinIndex].joins.length > 0) {
748
+ uri += "/$" + self.pathParts[rightJoinIndex].alias;
749
+ }
750
+ }
751
+
752
+ // from the facet with null to end, we have to add path parts in the same order.
753
+ for (i = rightJoinIndex + 1; i < self.pathParts.length; i++) {
754
+ var part = self.pathParts[i];
755
+
756
+ // add the join
757
+ if (part.joins.length > 0) {
758
+ uri += "/" + joinsStr(part, part.alias, i, false);
759
+ }
760
+
761
+ // add the facet and filters
762
+ temp = parsedPartsWithoutJoin[i];
763
+ if (temp.parsed) {
764
+ uri += "/" + temp.parsed;
765
+ }
766
+ }
767
+
768
+
769
+ return {
770
+ path: uri,
771
+ // TODO could be replaced with the function
772
+ pathPrefixAliasMapping: lastPathPartAliasMapping,
773
+ isUsingRightJoin: rightJoinIndex !== -1,
774
+ };
775
+ },
776
+
777
+ get ermrestCompactPath() {
778
+ if (this._ermrestCompactPath === undefined) {
779
+ var res = this.computeERMrestCompactPath();
780
+ this._ermrestCompactPath = res.path;
781
+ this._pathPrefixAliasMapping = res.pathPrefixAliasMapping;
782
+ this._isUsingRightJoin = res.isUsingRightJoin;
783
+ }
784
+ return this._ermrestCompactPath;
785
+ },
786
+
787
+ /**
788
+ * alias mapping for the last path part
789
+ * can be used for retrieving the existing sourcekey paths
790
+ * so we can refer to them instead of repeating the path.
791
+ *
792
+ * The returned object has the following attributes:
793
+ * - aliases: An object of sourcekey name to alias
794
+ * - lastIndex: The last index used for the aliases,
795
+ * aliases are written in format of <mainalias>_<index>
796
+ * and the lastIndex will make it easier to generate new ones if needed
797
+ * @type {Object}
798
+ */
799
+ get pathPrefixAliasMapping() {
800
+ if (this._pathPrefixAliasMapping === undefined) {
801
+ // this API will populate this
802
+ var dummy = this.ermrestCompactPath;
803
+ }
804
+ return this._pathPrefixAliasMapping;
805
+ },
806
+
807
+ /**
808
+ * whether we're using right outer join for parsing the location
809
+ * @type {boolean}
810
+ */
811
+ get isUsingRightJoin() {
812
+ if (this._isUsingRightJoin === undefined) {
813
+ // this API will populate this
814
+ var dummy = this.ermrestCompactPath;
815
+ }
816
+ return this._isUsingRightJoin;
817
+ },
818
+
819
+ /**
820
+ * Array of path parts
821
+ * @type {PathPart[]}
822
+ */
823
+ get pathParts() {
824
+ return this._pathParts;
825
+ },
826
+
827
+ /**
828
+ *
829
+ * @returns {String} The URI of ermrest service
830
+ */
831
+ get service() {
832
+ return this._service;
833
+ },
834
+
835
+ /**
836
+ *
837
+ * @returns {String} catalog id with version
838
+ */
839
+ get catalog() {
840
+ return this._catalogSnapshot;
841
+ },
842
+
843
+ /**
844
+ *
845
+ * @returns {String} just the catalog id without version
846
+ */
847
+ get catalogId() {
848
+ return this._catalog;
849
+ },
850
+
851
+ /**
852
+ *
853
+ * @returns {String} just the catalog version
854
+ */
855
+ get version() {
856
+ return this._version;
857
+ },
858
+
859
+ /**
860
+ * The datetime iso string representation of the catalog version
861
+ * @returns {String} the iso string or null if version is not specified
862
+ */
863
+ get versionAsISOString() {
864
+ if (this._versionAsISOString === undefined) {
865
+ this._versionAsISOString = HistoryService.snapshotToDatetimeISO(this._version, false);
866
+ }
867
+ return this._versionAsISOString;
868
+ },
869
+
870
+ /**
871
+ * API of the ermrest service.
872
+ * API includes entity, attribute, aggregate, attributegroup
873
+ * @type {String}
874
+ */
875
+ get api() {
876
+ return this._api;
877
+ },
878
+
879
+ /**
880
+ * The first schema name in the projection table, null if schema is not specified
881
+ * @type {string}
882
+ */
883
+ get rootSchemaName() {
884
+ return this._rootSchemaName;
885
+ },
886
+
887
+ /**
888
+ * The first table name in the projection table
889
+ * @type {string}
890
+ */
891
+ get rootTableName() {
892
+ return this._rootTableName;
893
+ },
894
+
895
+ /**
896
+ * the schema name which the uri referres to, null if schema is not specified
897
+ * @type {string}
898
+ */
899
+ get schemaName() {
900
+ if (this._schemaName === undefined) {
901
+ this._schemaName = this.lastJoin ? this.lastJoin.toSchema : this._rootSchemaName;
902
+ }
903
+ return this._schemaName;
904
+ },
905
+
906
+ /**
907
+ * the table name which the uri referres to
908
+ * @type {string}
909
+ */
910
+ get tableName() {
911
+ if (this._tableName === undefined) {
912
+ this._tableName = this.lastJoin ? this.lastJoin.toTable : this._rootTableName;
913
+ }
914
+ return this._tableName;
915
+ },
916
+
917
+ /**
918
+ * Name of the schema name for the base table of faceting
919
+ * @type {String}
920
+ */
921
+ get facetBaseSchemaName() {
922
+ if (this._baseSchemaName === undefined) {
923
+ var secondTolast = this.facetBasePathPart;
924
+ this._baseSchemaName = secondTolast ? secondTolast.schema : this.rootSchemaName;
925
+ }
926
+ return this._baseSchemaName;
927
+ },
928
+
929
+ /**
930
+ * Name of the base table for faceting
931
+ * @type{String}
932
+ */
933
+ get facetBaseTableName() {
934
+ if (this._baseTableName === undefined) {
935
+ var secondTolast = this.facetBasePathPart;
936
+ this._baseTableName = secondTolast ? secondTolast.table : this.rootTableName;
937
+ }
938
+ return this._baseTableName;
939
+ },
940
+
941
+ /**
942
+ * The alias that will be used for the base table that should be used for faceting
943
+ * @type {String}
944
+ */
945
+ get facetBaseTableAlias() {
946
+ if (this._facetBaseTableAlias === undefined) {
947
+ var secondTolast = this.facetBasePathPart;
948
+ this._facetBaseTableAlias = secondTolast ? secondTolast.alias : this.rootTableAlias;
949
+ }
950
+ return this._facetBaseTableAlias;
951
+ },
952
+
953
+ /**
954
+ * The alias that will be used for the root table
955
+ * @type {String}
956
+ */
957
+ get rootTableAlias () {
958
+ if (this._rootTableAlias === undefined) {
959
+ this._rootTableAlias = this.hasJoin ? "T" : this.mainTableAlias;
960
+ }
961
+ return this._rootTableAlias;
962
+ },
963
+
964
+ /**
965
+ * The alias that will be used for the main table (the projection table)
966
+ * @type {String}
967
+ */
968
+ get mainTableAlias() {
969
+ return _parserAliases.MAIN_TABLE;
970
+ },
971
+
972
+
973
+ /**
974
+ * filter is converted to the last join table (if uri has join)
975
+ * @returns {ParsedFilter} undefined if there is no filter
976
+ */
977
+ get filter() {
978
+ return this.lastPathPart ? this.lastPathPart.filter : undefined;
979
+ },
980
+
981
+ get filtersString() {
982
+ return this.lastPathPart ? this.lastPathPart.filtersString : undefined;
983
+ },
984
+
985
+ /**
986
+ * A dictionary of available query parameters
987
+ * @returns {Object}
988
+ */
989
+ get queryParams() {
990
+ return this._queryParams;
991
+ },
992
+
993
+ /**
994
+ * The query parameters string (key1=val1&key2=val2).
995
+ * @returns {String}
996
+ */
997
+ get queryParamsString() {
998
+ return this._queryParamsString;
999
+ },
1000
+
1001
+ /**
1002
+ * If there's a join(linking) at the end or not.
1003
+ * @return {boolean}
1004
+ */
1005
+ get hasJoin() {
1006
+ if (this._hasJoin === undefined) {
1007
+ var len = this.pathParts.length;
1008
+ this._hasJoin = (len > 1) || (len == 1 && this.pathParts[0].joins.length > 0);
1009
+ }
1010
+ return this._hasJoin;
1011
+ },
1012
+
1013
+ /**
1014
+ * The last join in the uri. Take a look at `joins` for structure of join object.
1015
+ * @return {ParsedJoin}
1016
+ */
1017
+ get lastJoin() {
1018
+ if (this.hasJoin) {
1019
+ var joins = this.pathParts[this.pathParts.length-1].joins;
1020
+ return joins[joins.length-1];
1021
+ }
1022
+ return null;
1023
+ },
1024
+
1025
+ /**
1026
+ * The last part of path. could be null
1027
+ * @type {PathPart}
1028
+ */
1029
+ get lastPathPart() {
1030
+ return this.pathParts.length > 0 ? this.pathParts[this.pathParts.length-1] : undefined;
1031
+ },
1032
+
1033
+ /**
1034
+ * The second to last part of path. This part is important because the
1035
+ * facets are based on this.
1036
+ * @type {PathPart}
1037
+ */
1038
+ get facetBasePathPart() {
1039
+ var len = this.pathParts.length;
1040
+ return len >= 2 ? this.pathParts[len-2] : undefined;
1041
+ },
1042
+
1043
+ /**
1044
+ * set the facet of the last path part
1045
+ * @param {any} json the json object of facets
1046
+ */
1047
+ set facets(json) {
1048
+ var newFacet;
1049
+ if (typeof json === 'object' && json !== null) {
1050
+ newFacet = new ParsedFacets(json, '');
1051
+ }
1052
+ if (!this.lastPathPart) {
1053
+ this._pathParts.push(new PathPart(this.mainTableAlias, [], this.rootSchemaName, this.rootTableName));
1054
+ }
1055
+ delete this.lastPathPart.facets;
1056
+ this.lastPathPart.facets = newFacet;
1057
+ this._setDirty();
1058
+ },
1059
+
1060
+ /**
1061
+ * facets object of the last path part
1062
+ * @type {ParsedFacets} facets object
1063
+ */
1064
+ get facets() {
1065
+ return this.lastPathPart ? this.lastPathPart.facets : undefined;
1066
+ },
1067
+
1068
+ /**
1069
+ * Change the custom facet of the last path part
1070
+ * @param {any} json
1071
+ */
1072
+ set customFacets(json) {
1073
+ var newFacet;
1074
+ if (typeof json === 'object' && json !== null) {
1075
+ newFacet = new CustomFacets(json, '');
1076
+ }
1077
+ if (!this.lastPathPart) {
1078
+ this._pathParts.push(new PathPart(this.mainTableAlias, [], this.rootSchemaName, this.rootTableName));
1079
+ }
1080
+ delete this.lastPathPart.customFacets;
1081
+ this.lastPathPart.customFacets = newFacet;
1082
+ this._setDirty();
1083
+ },
1084
+
1085
+ /**
1086
+ * the custom facet of the last path part
1087
+ * @type {CustomFacets}
1088
+ */
1089
+ get customFacets() {
1090
+ return this.lastPathPart ? this.lastPathPart.customFacets : undefined;
1091
+ },
1092
+
1093
+ /**
1094
+ * modifiers that are available in the uri.
1095
+ * @return {string}
1096
+ */
1097
+ get _modifiers() {
1098
+ return (this.sort ? this.sort : "") + (this.paging ? this.paging : "");
1099
+ },
1100
+
1101
+ /**
1102
+ * last searchTerm of the last path part
1103
+ * @returns {string} the search term
1104
+ */
1105
+ get searchTerm() {
1106
+ if (this.lastPathPart) {
1107
+ return this.lastPathPart.searchTerm;
1108
+ }
1109
+ return null;
1110
+ },
1111
+
1112
+ /**
1113
+ * if the location has visible facet/filter/customfacet
1114
+ * NOTE: if location only has hidden facets or custom facets without displayname,
1115
+ * this will return false.
1116
+ * @return {Boolean}]
1117
+ */
1118
+ get isConstrained() {
1119
+ return (this.facets && this.facets.hasVisibleFilters) || this.searchTerm || this.filter || (this.customFacets && this.customFacets.displayname);
1120
+ },
1121
+
1122
+ /**
1123
+ * Subject to change soon
1124
+ * @returns {String} The sort modifier in the string format of @sort(...)
1125
+ */
1126
+ get sort() {
1127
+ return this._sort;
1128
+ },
1129
+
1130
+ /**
1131
+ * get the sorting object, null if no sorting
1132
+ * @returns {Object[]} in this format [{"column":colname, "descending":true},...]
1133
+ */
1134
+ get sortObject() {
1135
+ if (this._sortObject === undefined) {
1136
+ if (this._sort !== undefined) {
1137
+ var sorts = this._sort.match(/@sort\(([^\)]*)\)/)[1].split(",");
1138
+
1139
+ this._sortObject = [];
1140
+ for (var s = 0; s < sorts.length; s++) {
1141
+ var sort = sorts[s];
1142
+ var column = (sort.endsWith("::desc::") ?
1143
+ decodeURIComponent(sort.match(/(.*)::desc::/)[1]) : decodeURIComponent(sort));
1144
+ this._sortObject.push({"column": column, "descending": sort.endsWith("::desc::")});
1145
+ }
1146
+ } else {
1147
+ this._sortObject = null;
1148
+ }
1149
+ }
1150
+ return this._sortObject;
1151
+ },
1152
+
1153
+ /**
1154
+ * change sort with new sort Object
1155
+ * @param {{ column: string; descending?: boolean }[] | null} so in this format [{"column":colname, "descending":true},...]
1156
+ */
1157
+ set sortObject(so) {
1158
+ if ((!so && !this._sort) || (so === this._sort))
1159
+ return;
1160
+
1161
+ // null or undefined = remove sort
1162
+ var oldSortString = (this._sort? this._sort : "");
1163
+ if (!so || so.length === 0) {
1164
+ delete this._sort;
1165
+ this._sortObject = null;
1166
+ } else {
1167
+ this._sortObject = so;
1168
+ this._sort = _getSortModifier(so);
1169
+ }
1170
+
1171
+ // enforce updating uri
1172
+ this._setDirty();
1173
+ },
1174
+
1175
+ /**
1176
+ * String representation of before: @before(..)
1177
+ * @type {string}
1178
+ */
1179
+ get before () {
1180
+ return this._before;
1181
+ },
1182
+
1183
+ /**
1184
+ * String representation of before: @after(..)
1185
+ * @type {string}
1186
+ */
1187
+ get after () {
1188
+ return this._after;
1189
+ },
1190
+
1191
+ /**
1192
+ *
1193
+ * @returns {String|undefined} The string format of the paging modifier in the form of @before(..)@after(...)
1194
+ */
1195
+ get paging() {
1196
+ if (this.after || this.before) {
1197
+ return (this.after ? this.after : "") + (this.before ? this.before : "");
1198
+ }
1199
+ return undefined;
1200
+ },
1201
+
1202
+ /**
1203
+ * array of values that is used for before
1204
+ * @return {Object[]?}
1205
+ */
1206
+ get beforeObject () {
1207
+ if (this._beforeObject === undefined) {
1208
+ var row, i, value;
1209
+ if (this._before) {
1210
+ this._beforeObject = [];
1211
+ row = this._before.match(/@before\(([^\)]*)\)/)[1].split(",");
1212
+
1213
+ // NOTE the number of values might be different from the sort
1214
+ // because columns can be sorted based on value of multiple columns
1215
+ for (i = 0; i < row.length; i++) {
1216
+ // ::null:: to null, empty string to "", otherwise decode value
1217
+ value = (row[i] === "::null::" ? null : decodeURIComponent(row[i]));
1218
+ this._beforeObject.push(value);
1219
+ }
1220
+ } else {
1221
+ this._beforeObject = null;
1222
+ }
1223
+ }
1224
+ return this._beforeObject;
1225
+ },
1226
+
1227
+ /**
1228
+ * change the beforeObject with new values
1229
+ * @param {Object[]?} Array of values that you want to page with.
1230
+ */
1231
+ set beforeObject(values) {
1232
+ // invalid argument, or empty string -> remove before
1233
+ if (!Array.isArray(values) || values.length === 0) {
1234
+ this._beforeObject = null;
1235
+ delete this._before;
1236
+ } else {
1237
+ if (this._sort) {
1238
+ this._beforeObject = values;
1239
+ this._before = _getPagingModifier(values, true);
1240
+ } else {
1241
+ throw new InvalidPageCriteria("Error setting before: Paging not allowed without sort", this.path);
1242
+ }
1243
+ }
1244
+
1245
+ // enforce updating uri
1246
+ this._setDirty();
1247
+ },
1248
+
1249
+ /**
1250
+ * array of values that is used for after
1251
+ * @return {Object[]?}
1252
+ */
1253
+ get afterObject () {
1254
+ if (this._afterObject === undefined) {
1255
+ var row, i, value;
1256
+ if (this._after) {
1257
+ this._afterObject = [];
1258
+ row = this._after.match(/@after\(([^\)]*)\)/)[1].split(",");
1259
+
1260
+ // NOTE the number of values might be different from the sort
1261
+ // because columns can be sorted based on value of multiple columns
1262
+ for (i = 0; i < row.length; i++) {
1263
+ // ::null:: to null, empty string to "", otherwise decode value
1264
+ value = (row[i] === "::null::" ? null : decodeURIComponent(row[i]));
1265
+ this._afterObject.push(value);
1266
+ }
1267
+ } else {
1268
+ this._afterObject = null;
1269
+ }
1270
+ }
1271
+ return this._afterObject;
1272
+ },
1273
+
1274
+ /**
1275
+ * change the paging with new afterObject
1276
+ * @param {Object[]?} Array of values that you want to page with.
1277
+ */
1278
+ set afterObject(values) {
1279
+ // invalid argument, or empty string -> remove after
1280
+ if (!Array.isArray(values) || values.length === 0) {
1281
+ this._afterObject = null;
1282
+ delete this._after;
1283
+ } else {
1284
+ if (this._sort) {
1285
+ this._afterObject = values;
1286
+ this._after = _getPagingModifier(values, false);
1287
+ } else {
1288
+ throw new InvalidPageCriteria("Error setting after: Paging not allowed without sort", this.path);
1289
+ }
1290
+ }
1291
+
1292
+ // enforce updating uri
1293
+ this._setDirty();
1294
+ },
1295
+
1296
+ /**
1297
+ * Apply, replace, clear filter term on the location
1298
+ * @param {string} term - optional, set or clear search
1299
+ */
1300
+ search: function(t) {
1301
+ var term = (t == null || t === "") ? null : t;
1302
+
1303
+ if (term === this.searchTerm) {
1304
+ return;
1305
+ }
1306
+
1307
+ var newSearchFacet = {"sourcekey": _specialSourceDefinitions.SEARCH_BOX, "search": [term]};
1308
+ var hasSearch = this.searchTerm != null;
1309
+ var hasFacets = this.facets != null;
1310
+ var andOperator = _FacetsLogicalOperators.AND;
1311
+
1312
+ var facetObject, andFilters;
1313
+ if (term === null) {
1314
+ // hasSearch must be true, if not there's something wrong with logic.
1315
+ // if term === null, that means the searchTerm is not null, therefore has search
1316
+ facetObject = [];
1317
+ this.facets.decoded[andOperator].forEach(function (f) {
1318
+ if (f.sourcekey !== _specialSourceDefinitions.SEARCH_BOX) {
1319
+ facetObject.push(f);
1320
+ }
1321
+ });
1322
+
1323
+ if (facetObject.length !== 0) {
1324
+ facetObject = {"and": facetObject};
1325
+ }
1326
+ } else {
1327
+ if (hasFacets) {
1328
+ facetObject = JSON.parse(JSON.stringify(this.facets.decoded));
1329
+ if (!hasSearch) {
1330
+ facetObject[andOperator].unshift(newSearchFacet);
1331
+ } else {
1332
+ andFilters = facetObject[andOperator];
1333
+ for (var i = 0; i < andFilters.length; i++) {
1334
+ if (andFilters[i].sourcekey === _specialSourceDefinitions.SEARCH_BOX) {
1335
+ if (Array.isArray(andFilters[i].search)) {
1336
+ andFilters[i].search = [term];
1337
+ }
1338
+ break;
1339
+ }
1340
+ }
1341
+ }
1342
+ } else {
1343
+ facetObject = {"and": [newSearchFacet]};
1344
+ }
1345
+ }
1346
+
1347
+ this.facets = (facetObject && facetObject.and) ? facetObject : null;
1348
+
1349
+ // enforce updating uri
1350
+ this._setDirty();
1351
+ },
1352
+
1353
+ /**
1354
+ * Remove the filters from the location
1355
+ */
1356
+ removeFilters: function () {
1357
+ if (this.lastPathPart) {
1358
+ delete this.lastPathPart.filter;
1359
+ delete this.lastPathPart.filtersString;
1360
+ this._setDirty();
1361
+ }
1362
+ },
1363
+
1364
+ /**
1365
+ * Create a new location object with the same uri and catalogObject
1366
+ * @param {Reference}
1367
+ * @returns {Location} new location object
1368
+ *
1369
+ * @private
1370
+ */
1371
+ _clone: function(referenceObject) {
1372
+ var res = parse(this.uri, this.catalogObject);
1373
+ if (isObjectAndNotNull(referenceObject)) {
1374
+ res.referenceObject = referenceObject;
1375
+ }
1376
+ return res;
1377
+ },
1378
+
1379
+ _setDirty: function() {
1380
+ delete this._uri;
1381
+ delete this._path;
1382
+ delete this._compactUri;
1383
+ delete this._compactPath;
1384
+ delete this._ermrestUri;
1385
+ delete this._ermrestPath;
1386
+ delete this._ermrestCompactUri;
1387
+ delete this._ermrestCompactPath;
1388
+ delete this._pathPrefixAliasMapping;
1389
+ },
1390
+
1391
+ /**
1392
+ * mechanism to pass catalog object to parser object.
1393
+ * parser initially was designed so it would just return the different
1394
+ * url sections, and it is the reference and other apis responsibility
1395
+ * to check the model (therefore parser didn't need to know anything about the model).
1396
+ * But now the facet part of url, needs information about the model structure since
1397
+ * we have to support the sourcekey syntax in there here.
1398
+ * TODO we migth be able to improve how this is setup.
1399
+ */
1400
+ set catalogObject(obj) {
1401
+ if (obj.id !== this.catalog) {
1402
+ throw new InvalidInputError("Given catalog object is not the same catalog used in the url.");
1403
+ }
1404
+ this._catalogObject = obj;
1405
+ },
1406
+
1407
+ /**
1408
+ * @type {catalog}
1409
+ */
1410
+ get catalogObject() {
1411
+ return this._catalogObject;
1412
+ },
1413
+
1414
+ /**
1415
+ * set the reference object that this Location object belongs to
1416
+ */
1417
+ set referenceObject(obj) {
1418
+ this._referenceObject = obj;
1419
+ },
1420
+
1421
+ /**
1422
+ * The reference object that this Location object belongs to
1423
+ * @type {Reference}
1424
+ */
1425
+ get referenceObject() {
1426
+ return this._referenceObject;
1427
+ },
1428
+
1429
+ /**
1430
+ * Given a sourceObjectWrapper, return a Location object that uses this
1431
+ * as the join to new table.
1432
+ * @param {Object} sourceObjectWrapper the object that represents the join
1433
+ * @param {string} toSchema the name of schema that this join refers to
1434
+ * @param {string} toTable the name of table that this join refers to
1435
+ * @param {boolean=} clone whether we should clone or use the existing object
1436
+ * @returns Location object
1437
+ */
1438
+ addJoin: function (sourceObjectWrapper, toSchema, toTable, clone) {
1439
+ var loc = clone ? this._clone() : this;
1440
+ // change alias of the previous part
1441
+ var lastPart = loc.lastPathPart;
1442
+
1443
+ /**
1444
+ * since we're adding a path part, we have to change the alias of previous part.
1445
+ * this alias is for the projected table. if there are joins, it will be added to the last
1446
+ * join and if there aren't, it will be attached to the schema:table.
1447
+ *
1448
+ * when there aren't any joins, this could be the schema:table, so we have to start with T.
1449
+ * but if there are joins, we can start with T1, as T alias is always the first schema:table.
1450
+ *
1451
+ * this is how we're using the alias property:
1452
+ * - if it doesn't have any joins, it's the alias of the previous path or root (so facet/)
1453
+ * - if it has join, it's the alias that we're using to name the projected table that the join represents.
1454
+ */
1455
+ if (lastPart) {
1456
+ var aliasNumber = "";
1457
+ if (lastPart.joins && lastPart.joins.length > 0) {
1458
+ if (loc.pathParts.length > 0) {
1459
+ aliasNumber = loc.pathParts.length;
1460
+ }
1461
+ }
1462
+ else if (loc.pathParts.length > 1) {
1463
+ aliasNumber = loc.pathParts.length - 1;
1464
+ }
1465
+
1466
+ var alias = _parserAliases.JOIN_TABLE_PREFIX + aliasNumber;
1467
+ lastPart.alias = alias;
1468
+ }
1469
+
1470
+ // add a join to parts based on the given input
1471
+ var pathPart = new PathPart(
1472
+ // make sure the proper alias is used for the last part
1473
+ _parserAliases.MAIN_TABLE,
1474
+ [new ParsedJoin(null, null, toSchema, toTable, null, sourceObjectWrapper)],
1475
+ toSchema,
1476
+ toTable
1477
+ );
1478
+
1479
+ loc._pathParts.push(pathPart);
1480
+
1481
+ loc._setDirty();
1482
+
1483
+ // make sure all the properties related to join are computed again
1484
+ delete loc._schemaName;
1485
+ delete loc._tableName;
1486
+ delete loc._baseSchemaName;
1487
+ delete loc._baseTableName;
1488
+ delete loc._facetBaseTableAlias;
1489
+ delete loc._rootTableAlias;
1490
+ delete loc._hasJoin;
1491
+
1492
+ return loc;
1493
+ }
1494
+ };
1495
+
1496
+ /**
1497
+ * Container for the path part, It will have the following attributes:
1498
+ * - joins: an array of ParsedJoin objects.
1499
+ * - alias: The alias that the facets should refer to
1500
+ * - if there aren't any joins, it's the alias of the previous path or root.
1501
+ * - otherwise, it's the alias that we're using to name the projected table that the join represents.
1502
+ * - schema: The schema that the join ends up with
1503
+ * - table: the table that the joins end up with
1504
+ * - facets: the facets object
1505
+ * - searchTerm: the search term that is in the facets
1506
+ * - customFacets: the custom facets objects.
1507
+ * - fitler: the filter object
1508
+ * - filtersString: the string representation of the filter
1509
+ * @param {String} alias The alias that the facets should refer to
1510
+ * @param {ParsedJoin[]} joins an array of ParsedJoin objects.
1511
+ * @param {String} schema The schema that the join ends up with
1512
+ * @param {String} table The table that the join ends up with
1513
+ * @param {ParsedFacets} facets the facets object
1514
+ * @param {CustomFacets} cfacets the custom facets object
1515
+ * @param {ParsedFilter} filter the filter object
1516
+ * @param {String} filtersString the string representation of the filter
1517
+ * @constructor
1518
+ */
1519
+ function PathPart(alias, joins, schema, table, facets, cfacets, filter, filtersString) {
1520
+ this._alias = alias;
1521
+ this.joins = Array.isArray(joins) ? joins : [];
1522
+ this.schema = (typeof schema === "string") ? schema : "";
1523
+ this.table = table;
1524
+ this._facets = facets;
1525
+ this.searchTerm = (facets && facets.decoded) ? _getSearchTerm(facets.decoded) : null;
1526
+ this.customFacets = cfacets;
1527
+ this.filter = filter;
1528
+ this.filtersString = filtersString;
1529
+ }
1530
+
1531
+ PathPart.prototype = {
1532
+ /**
1533
+ * Set the facets, this will take crea of the searchTerm attribute.
1534
+ */
1535
+ set facets(facets) {
1536
+ delete this._facets;
1537
+ this._facets = facets;
1538
+ this.searchTerm = (facets && facets.decoded) ? _getSearchTerm(facets.decoded) : null;
1539
+ },
1540
+
1541
+ get facets() {
1542
+ return this._facets;
1543
+ },
1544
+
1545
+ set alias(alias) {
1546
+ // sanity check to make sure code is working as expected
1547
+ if (!isStringAndNotEmpty(alias)) {
1548
+ throw new InvalidInputError("Given alias must be string.");
1549
+ }
1550
+ this._alias = alias;
1551
+ },
1552
+
1553
+ get alias () {
1554
+ return this._alias;
1555
+ }
1556
+ };
1557
+
1558
+ /**
1559
+ * given the string of parameters, create an object of them.
1560
+ *
1561
+ * @param {String} params the string representation of the query params
1562
+ * @returns {Object} the query params object
1563
+ * @private
1564
+ */
1565
+ export const _getQueryParams = function (params) {
1566
+ var queryParams = {},
1567
+ parts = params.split("&"),
1568
+ part, i;
1569
+ for (i = 0; i < parts.length; i++) {
1570
+ part = parts[i].split("=");
1571
+ queryParams[decodeURIComponent(part[0])] = decodeURIComponent(part[1]);
1572
+ }
1573
+ return queryParams;
1574
+ };
1575
+
1576
+ /**
1577
+ * for testingiven sort object, get the string modifier
1578
+ * @param {Object[]} sort [{"column":colname, "descending":boolean}, ...]
1579
+ * @return {string} string modifier @sort(...)
1580
+ * @private
1581
+ */
1582
+ export const _getSortModifier = function(sort) {
1583
+
1584
+ // if no sorting
1585
+ if (!sort || !Array.isArray(sort) || sort.length === 0) {
1586
+ return "";
1587
+ }
1588
+
1589
+ var modifier = "@sort(";
1590
+ for (var i = 0; i < sort.length; i++) {
1591
+ if (i !== 0) modifier = modifier + ",";
1592
+ if (!sort[i].column) throw new InvalidInputError("Invalid sort object.");
1593
+ modifier = modifier + fixedEncodeURIComponent(sort[i].column) + (sort[i].descending ? "::desc::" : "");
1594
+ }
1595
+ modifier = modifier + ")";
1596
+ return modifier;
1597
+ };
1598
+
1599
+ /**
1600
+ * given paging object, get the paging modifier
1601
+ * @param {Object} values the values
1602
+ * @param {boolean} indicates whether its before or after
1603
+ * @return {string} string modifier @after/before(...)
1604
+ * @private
1605
+ */
1606
+ export const _getPagingModifier = function(values, isBefore) {
1607
+
1608
+ // no paging
1609
+ if (!Array.isArray(values) || values.length === 0) {
1610
+ return "";
1611
+ }
1612
+
1613
+ var modifier = (isBefore ? "@before(" : "@after(");
1614
+ for (var i = 0; i < values.length; i++) {
1615
+ if (i !== 0) modifier = modifier + ",";
1616
+ modifier = modifier + ((values[i] === null || values[i] === undefined ) ? "::null::" : fixedEncodeURIComponent(values[i]));
1617
+ }
1618
+ modifier = modifier + ")";
1619
+ return modifier;
1620
+ };
1621
+
1622
+ /**
1623
+ * Given a term will return the filter string that ermrest understands
1624
+ * @param {string} term Search term
1625
+ * @param {string=} column the column that search is based on (if undefined, search on table).
1626
+ * @return {string} corresponding ermrest filter
1627
+ * @private
1628
+ */
1629
+ export const _convertSearchTermToFilter = function (term, column, alias, catalogObject) {
1630
+ var filterString = "";
1631
+
1632
+ // see if the quantified_value_lists syntax can be used
1633
+ var useQuantified = false;
1634
+ if (catalogObject) {
1635
+ if (column === _systemColumnNames.RID) {
1636
+ useQuantified = catalogObject.features[_ERMrestFeatures.QUANTIFIED_RID_LISTS];
1637
+ } else {
1638
+ useQuantified = catalogObject.features[_ERMrestFeatures.QUANTIFIED_VALUE_LISTS];
1639
+ }
1640
+ }
1641
+
1642
+ column = (typeof column !== 'string' || column === "*") ? "*": fixedEncodeURIComponent(column);
1643
+ if (isStringAndNotEmpty(alias)) {
1644
+ column = alias + ":" + column;
1645
+ }
1646
+
1647
+ if (term && term !== "") {
1648
+ // add a quote to the end if string has an odd amount
1649
+ if ( (term.split('"').length-1)%2 === 1 ) {
1650
+ term = term + '"';
1651
+ }
1652
+
1653
+ // 1) parse terms in quotation
1654
+ // 2) split the rest by space
1655
+ var terms = term.match(/"[^"]*"/g); // everything that's inside quotation
1656
+ if (!terms) terms = [];
1657
+ for (var i = 0; i < terms.length; i++) {
1658
+ term = term.replace(terms[i], ""); // remove from term
1659
+ terms[i] = terms[i].replace(/"/g, ""); //remove quotes
1660
+ }
1661
+
1662
+ if (term.trim().length > 0 ) terms = terms.concat(term.trim().split(/[\s]+/)); // split by white spaces
1663
+
1664
+ /**
1665
+ * This can happen if users search for `""` (for example `search "" term`)
1666
+ * it can also happen as a side effect of adding extra quote if there are
1667
+ * odd number of quote (for example `search "` will turn into `search ""`)
1668
+ */
1669
+ terms = terms.filter(function (term) { return isStringAndNotEmpty(term); });
1670
+
1671
+ // the quantified syntax only makes sense when we have more than one term
1672
+ if (terms.length < 2) useQuantified = false;
1673
+
1674
+ if (useQuantified) {
1675
+ filterString = column + _ERMrestFilterPredicates.CASE_INS_REG_EXP + 'all(';
1676
+ }
1677
+ terms.forEach(function(t, index, array) {
1678
+ var exp;
1679
+ // matches an integer, aka just a number
1680
+ if (t.match(/^[0-9]+$/)) {
1681
+ exp = "^(.*[^0-9.])?0*" + _encodeRegexp(t) + "([^0-9].*|$)";
1682
+ // matches a float, aka a number one decimal
1683
+ } else if (t.match(/^([0-9]+[.][0-9]*|[0-9]*[.][0-9]+)$/)) {
1684
+ exp = "^(.*[^0-9.])?0*" + _encodeRegexp(t);
1685
+ // matches everything else (words and anything with multiple decimals)
1686
+ } else {
1687
+ exp = _encodeRegexp(t);
1688
+ }
1689
+
1690
+ if (useQuantified) {
1691
+ filterString += (index === 0? "" : ",") + fixedEncodeURIComponent(exp);
1692
+ } else {
1693
+ filterString += (index === 0? "" : "&") + column + _ERMrestFilterPredicates.CASE_INS_REG_EXP + fixedEncodeURIComponent(exp);
1694
+ }
1695
+ });
1696
+ if (useQuantified) {
1697
+ filterString += ')';
1698
+ }
1699
+ }
1700
+
1701
+ return filterString;
1702
+ };
1703
+
1704
+ /**
1705
+ * Given the facetObject, find the `*` facet and extract the search term.
1706
+ * Should be called whenever we're changing the facet object
1707
+ * Will only consider the first `source`: `*`.
1708
+ * @param {object} facetObject the facet object
1709
+ * @return {string} search term
1710
+ */
1711
+ export const _getSearchTerm = function (facetObject) {
1712
+ var andFilters = facetObject[_FacetsLogicalOperators.AND],
1713
+ searchTerm = "";
1714
+
1715
+ for (var i = 0; i < andFilters.length; i++) {
1716
+ if (andFilters[i].sourcekey === _specialSourceDefinitions.SEARCH_BOX) {
1717
+ if (Array.isArray(andFilters[i].search)) {
1718
+ searchTerm = andFilters[i].search.join("|");
1719
+ }
1720
+ break;
1721
+ }
1722
+ }
1723
+ return searchTerm.length === 0 ? null : searchTerm;
1724
+ };
1725
+
1726
+ /**
1727
+ * The object representing a join
1728
+ * (could be multiple joins or just a single one)
1729
+ * @param {string?} str - the string representation (could have aliases)
1730
+ * @param {string?} strReverse - the reverse string representation
1731
+ * @param {string} toSchema
1732
+ * @param {string} toTable
1733
+ * @param {object?} colMapping - the column mapping info ({fromCols: [<string>], fromColsStr: <string>, toCols: [<string>, toColsStr: <string>]})
1734
+ * @param {object?} sourceObjectWrapper - the source object that represents the join
1735
+ */
1736
+ function ParsedJoin(str, strReverse, toSchema, toTable, colMapping, sourceObjectWrapper) {
1737
+ this.str = str;
1738
+ this.strReverse = strReverse;
1739
+ this.toSchema = toSchema;
1740
+ this.toTable = toTable;
1741
+ if (colMapping) {
1742
+ this.hasColumnMapping = true;
1743
+ this.fromCols = colMapping.fromCols;
1744
+ this.fromColsStr = colMapping.fromColsStr;
1745
+ this.toCols = colMapping.toCols;
1746
+ this.toColsStr = colMapping.toColsStr;
1747
+ }
1748
+
1749
+ if (sourceObjectWrapper) {
1750
+ this.sourceObjectWrapper = sourceObjectWrapper;
1751
+
1752
+ this.str = sourceObjectWrapper.toString();
1753
+ this.strReverse = sourceObjectWrapper.toString(true);
1754
+ }
1755
+
1756
+ // should not happen in the existing work flow, added just for sanity check
1757
+ if (!this.str || !this.strReverse) {
1758
+ throw new InvalidInputError("Either str/strReverse or sourceObjectWrapper must be defined.");
1759
+ }
1760
+ }
1761
+
1762
+ /**
1763
+ * Given the string representation of join create ParsedJoin
1764
+ * @param {string} linking - string representation
1765
+ * @param {string} table - from table
1766
+ * @param {string} schema - from schema
1767
+ * @returns {ParsedJoin}
1768
+ */
1769
+ function _createParsedJoinFromStr (linking, table, schema) {
1770
+ var fromSchemaTable = schema ? [schema,table].join(":") : table;
1771
+ var fromCols = linking[1].split(",");
1772
+ var toParts = linking[2].match(/([^:]*):([^:]*):([^\)]*)/);
1773
+ var toCols = toParts[3].split(",");
1774
+ var strReverse = "(" + toParts[3] + ")=(" + fromSchemaTable + ":" + linking[1] + ")";
1775
+
1776
+ return new ParsedJoin(
1777
+ linking[0], // str
1778
+ strReverse, // strReverse
1779
+ decodeURIComponent(toParts[1]), // toSchema
1780
+ decodeURIComponent(toParts[2]), // toTable
1781
+ {
1782
+ fromCols: fromCols.map(function(colName) {return decodeURIComponent(colName);}),
1783
+ fromColsStr: linking[1],
1784
+ toCols: toCols.map(function(colName) {return decodeURIComponent(colName);}),
1785
+ toColsStr: linking[2]
1786
+ } // columnMapping
1787
+ );
1788
+ }
1789
+
1790
+ /**
1791
+ *
1792
+ * A structure to store parsed filter
1793
+ *
1794
+ * { type: BinaryPredicate,
1795
+ * column: col_name,
1796
+ * operator: '=' or '::opr::'
1797
+ * value: value
1798
+ * }
1799
+ *
1800
+ * or
1801
+ *
1802
+ * { type: Conjunction or Disjunction
1803
+ * filters: [array of ParsedFilter]
1804
+ * }
1805
+ *
1806
+ *
1807
+ * @memberof ERMrest
1808
+ * @constructor
1809
+ * @param {String} type - type of filter
1810
+ * @desc
1811
+ * Constructor for a ParsedFilter.
1812
+ */
1813
+ export function ParsedFilter (type) {
1814
+ this.type = type;
1815
+ this.column = undefined;
1816
+ this.operator = undefined;
1817
+ this.value = undefined;
1818
+ this.filters = undefined;
1819
+ }
1820
+
1821
+ ParsedFilter.prototype = {
1822
+ constructor: ParsedFilter,
1823
+
1824
+ /**
1825
+ *
1826
+ * @param {ParsedFilter[]} filters array of binary predicate
1827
+ */
1828
+ setFilters: function(filters) {
1829
+ this.filters = filters;
1830
+ },
1831
+
1832
+ /**
1833
+ *
1834
+ * @param colname
1835
+ * @param operator '=', '::gt::', '::lt::', etc.
1836
+ * @param value
1837
+ */
1838
+ setBinaryPredicate: function(colname, operator, value) {
1839
+ this.column = colname;
1840
+ this.operator = operator;
1841
+ this.value = value;
1842
+ },
1843
+
1844
+ get facet() {
1845
+ if (this._facet === undefined) {
1846
+ this._toFacet();
1847
+ }
1848
+ return this._facet;
1849
+ },
1850
+
1851
+ get depth() {
1852
+ if (this._depth === undefined) {
1853
+ this._toFacet();
1854
+ }
1855
+ return this._depth;
1856
+ },
1857
+
1858
+ _toFacet: function () {
1859
+ var f = _filterToFacet(this);
1860
+ this._facet = f ? f.facet : null;
1861
+ this._depth = f ? f.depth : null;
1862
+ }
1863
+ };
1864
+
1865
+ function _processFilterPathPart(part, path) {
1866
+ // split by ';' and '&'
1867
+ var regExp = new RegExp('(;|&|[^;&]+)', 'g');
1868
+ var items = part.match(regExp);
1869
+
1870
+ if (!items) {
1871
+ throw new InvalidFilterOperatorError("Couldn't parse '" + part + "' in the url.", path, part);
1872
+ }
1873
+
1874
+ // if a single filter
1875
+ if (items.length === 1) {
1876
+ return _processSingleFilterString(items[0], path);
1877
+ }
1878
+
1879
+ var filters = [];
1880
+ var type = null;
1881
+ for (var i = 0; i < items.length; i++) {
1882
+ // process anything that's inside () first
1883
+ if (items[i].startsWith("(")) {
1884
+ items[i] = items[i].replace("(", "");
1885
+ // collect all filters until reaches ")"
1886
+ var subfilters = [];
1887
+ while (true) {
1888
+ if (items[i].endsWith(")")) {
1889
+ items[i] = items[i].replace(")", "");
1890
+ subfilters.push(items[i]);
1891
+ // get out of while loop
1892
+ break;
1893
+ } else {
1894
+ subfilters.push(items[i]);
1895
+ i++;
1896
+ }
1897
+ }
1898
+
1899
+ filters.push(_processMultiFilterString(subfilters, path));
1900
+
1901
+ } else if (type === null && items[i] === "&") {
1902
+ // first level filter type
1903
+ type = FILTER_TYPES.CONJUNCTION;
1904
+ } else if (type === null && items[i] === ";") {
1905
+ // first level filter type
1906
+ type = FILTER_TYPES.DISJUNCTION;
1907
+ } else if (type === FILTER_TYPES.CONJUNCTION && items[i] === ";") {
1908
+ // using combination of ! and & without ()
1909
+ throw new InvalidFilterOperatorError("Parser doesn't support combination of conjunction and disjunction filters.", path, part);
1910
+ } else if (type === FILTER_TYPES.DISJUNCTION && items[i] === "&") {
1911
+ // using combination of ! and & without ()
1912
+ throw new InvalidFilterOperatorError("Parser doesn't support combination of conjunction and disjunction filters.", path, part);
1913
+ } else if (items[i] !== "&" && items[i] !== ";") {
1914
+ // single filter on the first level
1915
+ var binaryFilter = _processSingleFilterString(items[i], path);
1916
+ filters.push(binaryFilter);
1917
+ }
1918
+ }
1919
+
1920
+ var filter = new ParsedFilter(type);
1921
+ filter.setFilters(filters);
1922
+ return filter;
1923
+ }
1924
+
1925
+ /**
1926
+ *
1927
+ * @param {stirng} filterString
1928
+ * @param {string} path used for redirect link generation
1929
+ * @returns {ParsedFilter} returns the parsed representation of the filter
1930
+ * @desc converts a filter string to ParsedFilter
1931
+ */
1932
+ function _processSingleFilterString(filterString, path) {
1933
+ //check for '=' or '::' to decide what split to use
1934
+ var f, filter;
1935
+ var throwError = function () {
1936
+ throw new InvalidFilterOperatorError("Couldn't parse '" + filterString + "' filter.", path, filterString);
1937
+ };
1938
+ if (filterString.indexOf("=") !== -1) {
1939
+ f = filterString.split('=');
1940
+ // NOTE: filter value (f[1]) can be empty
1941
+ if (f[0] && f.length === 2) {
1942
+ if (f[1] && f[1].startsWith('any(')) {
1943
+ if (!f[1].endsWith(')')) {
1944
+ throwError();
1945
+ }
1946
+ var vals = f[1].slice(4).slice(0,-1).split(",");
1947
+ if (vals.length === 0) {
1948
+ throwError();
1949
+ }
1950
+ filter = new ParsedFilter(FILTER_TYPES.DISJUNCTION);
1951
+ filter.setFilters(vals.map(function (v) {
1952
+ var temp = new ParsedFilter(FILTER_TYPES.BINARYPREDICATE);
1953
+ temp.setBinaryPredicate(decodeURIComponent(f[0]), "=", decodeURIComponent(v));
1954
+ return temp;
1955
+ }));
1956
+
1957
+ } else {
1958
+ filter = new ParsedFilter(FILTER_TYPES.BINARYPREDICATE);
1959
+ filter.setBinaryPredicate(decodeURIComponent(f[0]), "=", decodeURIComponent(f[1]));
1960
+ }
1961
+
1962
+ return filter;
1963
+ }
1964
+ } else {
1965
+ f = filterString.split("::");
1966
+ if (f.length === 3) {
1967
+ filter = new ParsedFilter(FILTER_TYPES.BINARYPREDICATE);
1968
+ filter.setBinaryPredicate(decodeURIComponent(f[0]), "::"+f[1]+"::", decodeURIComponent(f[2]));
1969
+ return filter;
1970
+ }
1971
+ }
1972
+ throwError();
1973
+ }
1974
+
1975
+ /**
1976
+ *
1977
+ * @param {String} filterStrings array representation of conjunction and disjunction of filters
1978
+ * without parenthesis. i.e., ['id=123', ';', 'id::gt::234', ';', 'id::le::345']
1979
+ * @param {string} path used for redirect link generation
1980
+ * @return {ParsedFilter}
1981
+ *
1982
+ */
1983
+ function _processMultiFilterString(filterStrings, path) {
1984
+ var filters = [];
1985
+ var type = null;
1986
+ for (var i = 0; i < filterStrings.length; i++) {
1987
+ if (type === null && filterStrings[i] === "&") {
1988
+ // first level filter type
1989
+ type = FILTER_TYPES.CONJUNCTION;
1990
+ } else if (type === null && filterStrings[i] === ";") {
1991
+ // first level filter type
1992
+ type = FILTER_TYPES.DISJUNCTION;
1993
+ } else if (type === FILTER_TYPES.CONJUNCTION && filterStrings[i] === ";") {
1994
+ // throw invalid filter error (using combination of ! and &)
1995
+ throw new InvalidFilterOperatorError("Couldn't parse '" + filterStrings + "' filter.", path, filterStrings);
1996
+ } else if (type === FILTER_TYPES.DISJUNCTION && filterStrings[i] === "&") {
1997
+ // throw invalid filter error (using combination of ! and &)
1998
+ throw new InvalidFilterOperatorError("Couldn't parse '" + filterStrings + "' filter.", path, filterStrings);
1999
+ } else if (filterStrings[i] !== "&" && filterStrings[i] !== ";") {
2000
+ // single filter on the first level
2001
+ var binaryFilter = _processSingleFilterString(filterStrings[i]);
2002
+ filters.push(binaryFilter);
2003
+ }
2004
+ }
2005
+
2006
+ var filter = new ParsedFilter(type);
2007
+ filter.setFilters(filters);
2008
+ return filter;
2009
+ }
2010
+
2011
+ /**
2012
+ * Given a parsedFilter object will return the corresponding facet.
2013
+ * If we cannot represent it with facet, it will return `null`.
2014
+ * Otherwise will return an object with
2015
+ * - depth: showing the depth of filter.
2016
+ * - facet: facet equivalent of the given filter
2017
+ *
2018
+ * @private
2019
+ * @param {Object} parsedFilter the filter
2020
+ * @return {Object}
2021
+ */
2022
+ function _filterToFacet(parsedFilter) {
2023
+ var res = _filterToFacetRec(parsedFilter, 0);
2024
+
2025
+ // could not be parsed
2026
+ if (!res) return null;
2027
+
2028
+ var depth = res.depth, facet = res.facet;
2029
+
2030
+ // if facet didn't have any operator, then we create one with and
2031
+ if (!("or" in facet) && !("and" in facet)) {
2032
+ facet = {and: [simpleDeepCopy(facet)]};
2033
+ depth = 1;
2034
+ }
2035
+
2036
+ return {facet: facet, depth: depth};
2037
+ }
2038
+
2039
+ // does the process of changing filter to facet recursively
2040
+ function _filterToFacetRec(parsedFilter, depth) {
2041
+ var facet = {}, orSources = {}, parsed, op, i, f, nextRes, parentDepth;
2042
+
2043
+ // base for binary predicate filters
2044
+ if (parsedFilter instanceof ParsedFilter && parsedFilter.type === FILTER_TYPES.BINARYPREDICATE){
2045
+ facet.source = parsedFilter.column;
2046
+ switch (parsedFilter.operator) {
2047
+ case _ERMrestFilterPredicates.GREATER_THAN_OR_EQUAL_TO:
2048
+ facet[_facetFilterTypes.RANGE] = [{min: parsedFilter.value}];
2049
+ break;
2050
+ case _ERMrestFilterPredicates.LESS_THAN_OR_EQUAL_TO:
2051
+ facet[_facetFilterTypes.RANGE] = [{max: parsedFilter.value}];
2052
+ break;
2053
+ case _ERMrestFilterPredicates.GREATER_THAN:
2054
+ facet[_facetFilterTypes.RANGE] = [{min: parsedFilter.value, min_exclusive: true}];
2055
+ break;
2056
+ case _ERMrestFilterPredicates.LESS_THAN:
2057
+ facet[_facetFilterTypes.RANGE] = [{max: parsedFilter.value, max_exclusive: true}];
2058
+ break;
2059
+ case _ERMrestFilterPredicates.NULL:
2060
+ facet[_facetFilterTypes.CHOICE] = [null];
2061
+ break;
2062
+ case _ERMrestFilterPredicates.CASE_INS_REG_EXP:
2063
+ facet[_facetFilterTypes.SEARCH] = [parsedFilter.value];
2064
+ break;
2065
+ case _ERMrestFilterPredicates.EQUAL:
2066
+ facet[[_facetFilterTypes.CHOICE]] = [parsedFilter.value];
2067
+ break;
2068
+ default:
2069
+ // operator is not supported by facet
2070
+ return null;
2071
+ }
2072
+
2073
+ return {facet: facet, depth: depth};
2074
+ }
2075
+
2076
+ if (Array.isArray(parsedFilter.filters)) {
2077
+
2078
+ // we're going one level deeper (since it's an array it will be turned into object of sources)
2079
+ depth++;
2080
+
2081
+ // set the filter type
2082
+ if (parsedFilter.type === FILTER_TYPES.DISJUNCTION) {
2083
+ op = "or";
2084
+ } else if (parsedFilter.type === FILTER_TYPES.CONJUNCTION) {
2085
+ op = "and";
2086
+ } else {
2087
+ return null;
2088
+ }
2089
+
2090
+ // will add the facets in the parsed to facet object
2091
+ var mergeFacets = function (c) {
2092
+ if (!parsed[c]) return;
2093
+ if (!facet[op][index][c]) facet[op][index][c] = [];
2094
+ facet[op][index][c].push(parsed[c][0]);
2095
+ };
2096
+
2097
+ parentDepth = depth;
2098
+ facet[op] = [];
2099
+ for (i = 0; i < parsedFilter.filters.length; i++) {
2100
+ f = parsedFilter.filters[i];
2101
+
2102
+ // get the facet for this child filter
2103
+ nextRes = _filterToFacetRec(f, parentDepth);
2104
+
2105
+ // couldn't parse it.
2106
+ if (!nextRes) return null;
2107
+
2108
+ parsed = nextRes.facet;
2109
+
2110
+ // depth of the parent will be the maximum depth of its children
2111
+ depth = Math.max(depth, nextRes.depth);
2112
+
2113
+
2114
+ // if operator is or and the filter is binary we can merge them
2115
+ // for example id=1;id=2 can turned into {source: "id", choices: ["1", "2"]}
2116
+ // or id=1;id::geq::2 can be {source: "id", "choices": ["1"], "ranges": [{min: 2}]}
2117
+ if (op === "or" && f.type === FILTER_TYPES.BINARYPREDICATE) {
2118
+ if (orSources[parsed.source] > -1) {
2119
+ // the source existed before, so it can be merged
2120
+ var index = orSources[parsed.source];
2121
+ _facetFilterTypeNames.forEach(mergeFacets);
2122
+ continue;
2123
+ } else {
2124
+ orSources[parsed.source] = facet[op].length;
2125
+ }
2126
+ }
2127
+
2128
+ // add the facet into the list
2129
+ facet[op].push(parsed);
2130
+ }
2131
+
2132
+ // if it's just one value, then we can just flatten the array and return that value.
2133
+ // the wrapper function will take care of adding the op, if it didn't exist
2134
+ if (facet[op].length === 1) {
2135
+ facet = facet[op][0];
2136
+ depth--;
2137
+ }
2138
+
2139
+ return {facet: facet, depth: depth};
2140
+ }
2141
+
2142
+ // invalid filter
2143
+ return null;
2144
+ }
2145
+
2146
+ /**
2147
+ * The complete structure of ermrest JSON filter is as follows:
2148
+ *
2149
+ * ```
2150
+ * <FILTERS>: { <logical-operator>: <TERMSET> }
2151
+ * <TERMSET>: '[' <TERM> [, <TERM>]* ']'
2152
+ *
2153
+ * <TERM>: { <logical-operator>: <TERMSET> }
2154
+ * or
2155
+ * { "source": <data-source>, <constraint(s)> }
2156
+ * ```
2157
+ *
2158
+ * But currently it only supports the following:
2159
+ *
2160
+ * {
2161
+ * "and": [
2162
+ * {
2163
+ * "source": <data-source>,
2164
+ * "choices": [v, ...],
2165
+ * "ranges": [{"min": v1, "max": v2}, ...],
2166
+ * "search": [v, ...],
2167
+ * "not_null": true
2168
+ * },
2169
+ * ...
2170
+ * ]
2171
+ * }
2172
+ *
2173
+ * <data-source> can be any of :
2174
+ * * -> the filter is in table level (not column)
2175
+ * string -> the filter is referring to a column (this is its name)
2176
+ * array -> the filter is referring to a column through a set of joins.
2177
+ * - each element shoud have
2178
+ * - An "inbound" or "outbound" key. Its value must be the constraint name array.
2179
+ * - last element must be a string, which is the column name.
2180
+ * for example:
2181
+ * [{"inbound": ["s1", "fk1"]}, {"outbound": ["s2", "fk2"]}, "col"]
2182
+ *
2183
+ * For detailed explanation take a look at the following link:
2184
+ * https://github.com/informatics-isi-edu/ermrestjs/issues/447
2185
+ *
2186
+ * @param {String|Object} str Can be blob or json (object).
2187
+ * @param {String} path to generate rediretUrl in error
2188
+ * @constructor
2189
+ */
2190
+ function ParsedFacets (str, path) {
2191
+
2192
+ if (typeof str === 'object') {
2193
+ /**
2194
+ * encode JSON object that represents facets
2195
+ * @type {any}
2196
+ */
2197
+ this.decoded = str;
2198
+
2199
+ /**
2200
+ * JSON object that represents facets
2201
+ * @type {string}
2202
+ */
2203
+ this.encoded = encodeFacet(str);
2204
+ } else {
2205
+ this.encoded = str;
2206
+ this.decoded = decodeFacet(str, path);
2207
+ }
2208
+
2209
+ var andOperator = _FacetsLogicalOperators.AND, obj = this.decoded;
2210
+ if (!Object.prototype.hasOwnProperty.call(obj, andOperator) || !Array.isArray(obj[andOperator])) {
2211
+ // we cannot actually parse the facet now, because we haven't
2212
+ // introspected the whole catalog yet, and don't have access to the constraint objects.
2213
+ throw new InvalidFacetOperatorError(path, _facetingErrors.invalidBooleanOperator);
2214
+ }
2215
+
2216
+ /**
2217
+ * Whether facet blob has any visible filters
2218
+ * @type {boolean}
2219
+ */
2220
+ this.hasVisibleFilters = obj[andOperator].some(function (f) {
2221
+ return !f.hidden;
2222
+ });
2223
+
2224
+ /**
2225
+ * Whether facet blob has any visible filters that is not based on search-box
2226
+ * @type {boolean}
2227
+ */
2228
+ this.hasNonSearchBoxVisibleFilters = obj[andOperator].some(function (f) {
2229
+ return !f.hidden && (!f.sourcekey || f.sourcekey !== _specialSourceDefinitions.SEARCH_BOX);
2230
+ });
2231
+
2232
+ /**
2233
+ * and array of conjunctive filters defined in the facet blob
2234
+ * @type {Array}
2235
+ */
2236
+ this.andFilters = obj[andOperator];
2237
+ }
2238
+ /**
2239
+ * An object that will have the follwoing attributes:
2240
+ * - facets: "the facet object"
2241
+ * - ermrest_path: "ermrest path string"
2242
+ * - displayname: {value: "the value", isHTML: boolean} (optional)
2243
+ *
2244
+ *
2245
+ * @param {String|Object} str Can be blob or json (object).
2246
+ * @param {String} path to generate rediretUrl in error
2247
+ * @constructor
2248
+ */
2249
+ function CustomFacets (str, path) {
2250
+
2251
+ // the eror that we should throw if it's invalid
2252
+ var error = new InvalidCustomFacetOperatorError('', path);
2253
+
2254
+ if (typeof str === 'object') {
2255
+ /**
2256
+ * encode JSON object that represents facets
2257
+ * @type {object}
2258
+ */
2259
+ this.decoded = str;
2260
+
2261
+ /**
2262
+ * JSON object that represents facets
2263
+ * @type {string}
2264
+ */
2265
+ this.encoded = encodeFacet(str);
2266
+ } else {
2267
+ this.encoded = str;
2268
+
2269
+ try {
2270
+ this.decoded = decodeFacet(str, path);
2271
+ } catch(exp) {
2272
+ // the exp will be InvalidFacetOperatorError, so we should change it to custom-facet
2273
+ throw error;
2274
+ }
2275
+ }
2276
+
2277
+ var obj = this.decoded;
2278
+ if ((!Object.prototype.hasOwnProperty.call(obj, "facets") && !Object.prototype.hasOwnProperty.call(obj, "ermrest_path"))) {
2279
+ throw error;
2280
+ }
2281
+
2282
+ if (obj.facets) {
2283
+ /**
2284
+ * the facet that this custom facet is representing
2285
+ * @type {ParsedFacets}
2286
+ */
2287
+ this.facets = new ParsedFacets(obj.facets);
2288
+ }
2289
+
2290
+ if (typeof obj.ermrest_path === "string") {
2291
+ /**
2292
+ * The ermrset string path that will be appended to the url
2293
+ * @type {String}
2294
+ */
2295
+ this.ermrestPath = trimSlashes(obj.ermrest_path);
2296
+ }
2297
+
2298
+ this.removable = true;
2299
+ if (typeof obj.removable === "boolean") {
2300
+ /**
2301
+ * Whether user can remove the facet or not
2302
+ * @type {string}
2303
+ */
2304
+ this.removable = obj.removable;
2305
+ }
2306
+
2307
+ if (isStringAndNotEmpty(obj.displayname)) {
2308
+ /**
2309
+ * The name that should be used to represent the facet value (optional)
2310
+ * @type {Object}
2311
+ */
2312
+ this.displayname = {
2313
+ value: obj.displayname,
2314
+ unformatted: obj.displayname,
2315
+ isHTML: false
2316
+ };
2317
+ } else if (isObjectAndNotNull(obj.displayname) && ("value" in obj.displayname)) {
2318
+ this.displayname = obj.displayname;
2319
+ }
2320
+ }