@isrd-isi-edu/ermrestjs 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +55 -0
- package/dist/ermrest.d.ts +3481 -0
- package/dist/ermrest.js +45 -0
- package/dist/ermrest.js.gz +0 -0
- package/dist/ermrest.js.map +1 -0
- package/dist/ermrest.min.js +45 -0
- package/dist/ermrest.min.js.gz +0 -0
- package/dist/ermrest.min.js.map +1 -0
- package/dist/ermrest.ver.txt +1 -0
- package/dist/stats.html +4949 -0
- package/js/ag_reference.js +1483 -0
- package/js/core.js +4931 -0
- package/js/datapath.js +336 -0
- package/js/export.js +956 -0
- package/js/filters.js +192 -0
- package/js/format.js +344 -0
- package/js/hatrac.js +1130 -0
- package/js/json_ld_validator.js +285 -0
- package/js/parser.js +2320 -0
- package/js/setup/node.js +27 -0
- package/js/utils/helpers.js +2300 -0
- package/js/utils/json_ld_schema.js +680 -0
- package/js/utils/pseudocolumn_helpers.js +2196 -0
- package/package.json +79 -0
- package/src/index.ts +204 -0
- package/src/models/comment.ts +14 -0
- package/src/models/deferred-promise.ts +16 -0
- package/src/models/display-name.ts +5 -0
- package/src/models/errors.ts +408 -0
- package/src/models/path-prefix-alias-mapping.ts +130 -0
- package/src/models/reference/bulk-create-foreign-key-object.ts +133 -0
- package/src/models/reference/citation.ts +98 -0
- package/src/models/reference/contextualize.ts +535 -0
- package/src/models/reference/google-dataset-metadata.ts +72 -0
- package/src/models/reference/index.ts +14 -0
- package/src/models/reference/page.ts +520 -0
- package/src/models/reference/reference-aggregate-fn.ts +37 -0
- package/src/models/reference/reference.ts +2813 -0
- package/src/models/reference/related-reference.ts +467 -0
- package/src/models/reference/tuple.ts +652 -0
- package/src/models/reference-column/asset-pseudo-column.ts +498 -0
- package/src/models/reference-column/column-aggregate.ts +313 -0
- package/src/models/reference-column/facet-column.ts +1380 -0
- package/src/models/reference-column/foreign-key-pseudo-column.ts +626 -0
- package/src/models/reference-column/inbound-foreign-key-pseudo-column.ts +131 -0
- package/src/models/reference-column/index.ts +13 -0
- package/src/models/reference-column/key-pseudo-column.ts +236 -0
- package/src/models/reference-column/pseudo-column.ts +850 -0
- package/src/models/reference-column/reference-column.ts +740 -0
- package/src/models/source-object-node.ts +156 -0
- package/src/models/source-object-wrapper.ts +694 -0
- package/src/models/table-source-definitions.ts +98 -0
- package/src/services/authn.ts +43 -0
- package/src/services/catalog.ts +37 -0
- package/src/services/config.ts +202 -0
- package/src/services/error.ts +247 -0
- package/src/services/handlebars.ts +607 -0
- package/src/services/history.ts +136 -0
- package/src/services/http.ts +536 -0
- package/src/services/logger.ts +70 -0
- package/src/services/mustache.ts +0 -0
- package/src/utils/column-utils.ts +308 -0
- package/src/utils/constants.ts +526 -0
- package/src/utils/markdown-utils.ts +855 -0
- package/src/utils/reference-utils.ts +1658 -0
- package/src/utils/template-utils.ts +0 -0
- package/src/utils/type-utils.ts +89 -0
- package/src/utils/value-utils.ts +127 -0
- package/tsconfig.json +30 -0
- package/vite.config.mts +104 -0
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
|
+
}
|