@isrd-isi-edu/ermrestjs 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +55 -0
- package/dist/ermrest.d.ts +3481 -0
- package/dist/ermrest.js +45 -0
- package/dist/ermrest.js.gz +0 -0
- package/dist/ermrest.js.map +1 -0
- package/dist/ermrest.min.js +45 -0
- package/dist/ermrest.min.js.gz +0 -0
- package/dist/ermrest.min.js.map +1 -0
- package/dist/ermrest.ver.txt +1 -0
- package/dist/stats.html +4949 -0
- package/js/ag_reference.js +1483 -0
- package/js/core.js +4931 -0
- package/js/datapath.js +336 -0
- package/js/export.js +956 -0
- package/js/filters.js +192 -0
- package/js/format.js +344 -0
- package/js/hatrac.js +1130 -0
- package/js/json_ld_validator.js +285 -0
- package/js/parser.js +2320 -0
- package/js/setup/node.js +27 -0
- package/js/utils/helpers.js +2300 -0
- package/js/utils/json_ld_schema.js +680 -0
- package/js/utils/pseudocolumn_helpers.js +2196 -0
- package/package.json +79 -0
- package/src/index.ts +204 -0
- package/src/models/comment.ts +14 -0
- package/src/models/deferred-promise.ts +16 -0
- package/src/models/display-name.ts +5 -0
- package/src/models/errors.ts +408 -0
- package/src/models/path-prefix-alias-mapping.ts +130 -0
- package/src/models/reference/bulk-create-foreign-key-object.ts +133 -0
- package/src/models/reference/citation.ts +98 -0
- package/src/models/reference/contextualize.ts +535 -0
- package/src/models/reference/google-dataset-metadata.ts +72 -0
- package/src/models/reference/index.ts +14 -0
- package/src/models/reference/page.ts +520 -0
- package/src/models/reference/reference-aggregate-fn.ts +37 -0
- package/src/models/reference/reference.ts +2813 -0
- package/src/models/reference/related-reference.ts +467 -0
- package/src/models/reference/tuple.ts +652 -0
- package/src/models/reference-column/asset-pseudo-column.ts +498 -0
- package/src/models/reference-column/column-aggregate.ts +313 -0
- package/src/models/reference-column/facet-column.ts +1380 -0
- package/src/models/reference-column/foreign-key-pseudo-column.ts +626 -0
- package/src/models/reference-column/inbound-foreign-key-pseudo-column.ts +131 -0
- package/src/models/reference-column/index.ts +13 -0
- package/src/models/reference-column/key-pseudo-column.ts +236 -0
- package/src/models/reference-column/pseudo-column.ts +850 -0
- package/src/models/reference-column/reference-column.ts +740 -0
- package/src/models/source-object-node.ts +156 -0
- package/src/models/source-object-wrapper.ts +694 -0
- package/src/models/table-source-definitions.ts +98 -0
- package/src/services/authn.ts +43 -0
- package/src/services/catalog.ts +37 -0
- package/src/services/config.ts +202 -0
- package/src/services/error.ts +247 -0
- package/src/services/handlebars.ts +607 -0
- package/src/services/history.ts +136 -0
- package/src/services/http.ts +536 -0
- package/src/services/logger.ts +70 -0
- package/src/services/mustache.ts +0 -0
- package/src/utils/column-utils.ts +308 -0
- package/src/utils/constants.ts +526 -0
- package/src/utils/markdown-utils.ts +855 -0
- package/src/utils/reference-utils.ts +1658 -0
- package/src/utils/template-utils.ts +0 -0
- package/src/utils/type-utils.ts +89 -0
- package/src/utils/value-utils.ts +127 -0
- package/tsconfig.json +30 -0
- package/vite.config.mts +104 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export default class HistoryService {
|
|
2
|
+
/**
|
|
3
|
+
* convert ISO datetime string to snapshot version string
|
|
4
|
+
* @throws {Error} Might throw some errors if the input is invalid
|
|
5
|
+
*/
|
|
6
|
+
static datetimeISOToSnapshot(value: string): string {
|
|
7
|
+
return HistoryService.urlb32Encode(HistoryService.datetimeEpochUs(value));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* convert snapshot version string to ISO datetime string
|
|
12
|
+
* @param {string} snapshot - the snapshot version string
|
|
13
|
+
* @param {boolean} dontThrowError - if true, will return empty string instead of throwing error
|
|
14
|
+
* when the input is invalid
|
|
15
|
+
* @returns {string} the ISO datetime string
|
|
16
|
+
* @throws {Error} Might throw some errors if the input is invalid
|
|
17
|
+
*/
|
|
18
|
+
static snapshotToDatetimeISO(snapshot: string, dontThrowError?: boolean): string {
|
|
19
|
+
try {
|
|
20
|
+
const epochUs = HistoryService.urlb32Decode(snapshot);
|
|
21
|
+
return HistoryService.epochUsToIso(epochUs);
|
|
22
|
+
} catch (e) {
|
|
23
|
+
if (dontThrowError) return '';
|
|
24
|
+
else throw e;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Return microseconds-since-epoch integer for given ISO datetime string
|
|
30
|
+
*/
|
|
31
|
+
static datetimeEpochUs(isoString: string): bigint {
|
|
32
|
+
// Parse the ISO string to extract microseconds
|
|
33
|
+
const match = isoString.match(/^(.+?)(?:\.(\d{1,6}))?([+-]\d{2}:?\d{2}|Z)$/);
|
|
34
|
+
if (!match) {
|
|
35
|
+
throw new Error(`Invalid ISO datetime format: ${isoString}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const [, baseTime, fractionalSeconds = '0', timezone] = match;
|
|
39
|
+
|
|
40
|
+
// Pad fractional seconds to 6 digits (microseconds)
|
|
41
|
+
const microsecondStr = fractionalSeconds.padEnd(6, '0').slice(0, 6);
|
|
42
|
+
const microseconds = BigInt(microsecondStr);
|
|
43
|
+
|
|
44
|
+
// Create date from the base time + timezone
|
|
45
|
+
const dt = new Date(baseTime + timezone);
|
|
46
|
+
const timestampMs = BigInt(dt.getTime());
|
|
47
|
+
|
|
48
|
+
return timestampMs * 1000n + microseconds;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Encode integer as per ERMrest's base-32 snapshot encoding
|
|
53
|
+
*/
|
|
54
|
+
static urlb32Encode(i: bigint): string {
|
|
55
|
+
if (i > 2n ** 63n - 1n) {
|
|
56
|
+
throw new Error(`Value ${i} exceeds maximum`);
|
|
57
|
+
} else if (i < -(2n ** 63n)) {
|
|
58
|
+
throw new Error(`Value ${i} below minimum`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// pad 64 bit to 65 bits for 13 5-bit digits
|
|
62
|
+
let raw = i << 1n;
|
|
63
|
+
const encodedRev: string[] = [];
|
|
64
|
+
|
|
65
|
+
for (let d = 1; d <= 13; d++) {
|
|
66
|
+
if (d > 2 && (d - 1) % 4 === 0) {
|
|
67
|
+
encodedRev.push('-');
|
|
68
|
+
}
|
|
69
|
+
const code = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'[Number(raw % 32n)];
|
|
70
|
+
encodedRev.push(code);
|
|
71
|
+
raw = raw / 32n; // Integer division with BigInt
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Remove trailing '0' and '-' characters
|
|
75
|
+
while (encodedRev.length > 0 && (encodedRev[encodedRev.length - 1] === '0' || encodedRev[encodedRev.length - 1] === '-')) {
|
|
76
|
+
encodedRev.pop();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (encodedRev.length === 0) {
|
|
80
|
+
encodedRev.push('0');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const encoded = encodedRev.reverse();
|
|
84
|
+
return encoded.join('');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Decode base-32 snapshot encoding back to integer
|
|
89
|
+
*/
|
|
90
|
+
static urlb32Decode(encoded: string): bigint {
|
|
91
|
+
const code = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
92
|
+
const codeMap: { [key: string]: number } = {};
|
|
93
|
+
for (let i = 0; i < code.length; i++) {
|
|
94
|
+
codeMap[code[i]] = i;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let result = 0n;
|
|
98
|
+
|
|
99
|
+
for (const char of encoded) {
|
|
100
|
+
if (char === '-') {
|
|
101
|
+
continue; // Skip separator characters
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!(char in codeMap)) {
|
|
105
|
+
throw new Error(`Invalid character in encoded string: ${char}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
result = result * 32n + BigInt(codeMap[char]);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Reverse the 65-bit padding (shift right by 1)
|
|
112
|
+
return result >> 1n;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Convert microseconds-since-epoch back to ISO datetime string
|
|
117
|
+
*/
|
|
118
|
+
static epochUsToIso(epochUs: bigint): string {
|
|
119
|
+
const epochMs = epochUs / 1000n;
|
|
120
|
+
const microseconds = epochUs % 1000n;
|
|
121
|
+
|
|
122
|
+
const dt = new Date(Number(epochMs));
|
|
123
|
+
|
|
124
|
+
// Format to ISO string and insert microseconds
|
|
125
|
+
// ISO format is: YYYY-MM-DDTHH:mm:ss.sssZ
|
|
126
|
+
// We need to replace the .sss with .sssuuu (milliseconds + microseconds)
|
|
127
|
+
const isoString = dt.toISOString();
|
|
128
|
+
|
|
129
|
+
// Insert microseconds into the ISO string
|
|
130
|
+
const milliseconds = dt.getUTCMilliseconds();
|
|
131
|
+
const totalMicroseconds = milliseconds * 1000 + Number(microseconds);
|
|
132
|
+
const microsecondsStr = totalMicroseconds.toString().padStart(6, '0');
|
|
133
|
+
|
|
134
|
+
return isoString.replace(/\.\d{3}Z$/, `.${microsecondsStr}+00:00`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
import { Deferred } from 'q';
|
|
2
|
+
|
|
3
|
+
// models
|
|
4
|
+
// import DeferredPromise from '@isrd-isi-edu/ermrestjs/src/models/deferred-promise';
|
|
5
|
+
|
|
6
|
+
// services
|
|
7
|
+
import CatalogService from '@isrd-isi-edu/ermrestjs/src/services/catalog';
|
|
8
|
+
import ConfigService from '@isrd-isi-edu/ermrestjs/src/services/config';
|
|
9
|
+
|
|
10
|
+
// utils
|
|
11
|
+
import { _shorterVersion, CONTEXT_HEADER_LENGTH_LIMIT, contextHeaderName } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
|
|
12
|
+
import { fixedEncodeURIComponent, simpleDeepCopy } from '@isrd-isi-edu/ermrestjs/src/utils/value-utils';
|
|
13
|
+
|
|
14
|
+
// legacy
|
|
15
|
+
import { getElapsedTime, onload } from '@isrd-isi-edu/ermrestjs/js/setup/node';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Enumeration of HTTP Response Status Codes, which are used within this
|
|
19
|
+
* sub-module. For internal use only.
|
|
20
|
+
*/
|
|
21
|
+
const _http_status_codes = {
|
|
22
|
+
no_connection: -1,
|
|
23
|
+
timed_out: 0,
|
|
24
|
+
no_content: 204,
|
|
25
|
+
unauthorized: 401,
|
|
26
|
+
not_found: 404,
|
|
27
|
+
internal_server_error: 500,
|
|
28
|
+
service_unavailable: 503,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Retriable error codes. These can sometimes indicate transient errors
|
|
33
|
+
* that can be resolved simply by retrying the request, up to a limit.
|
|
34
|
+
*/
|
|
35
|
+
const _retriable_error_codes = [
|
|
36
|
+
_http_status_codes.no_connection,
|
|
37
|
+
_http_status_codes.timed_out,
|
|
38
|
+
_http_status_codes.internal_server_error,
|
|
39
|
+
_http_status_codes.service_unavailable,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Mapping from convenience method name to its config argument index.
|
|
44
|
+
*/
|
|
45
|
+
const _method_to_config_idx = {
|
|
46
|
+
get: 1,
|
|
47
|
+
delete: 1,
|
|
48
|
+
head: 1,
|
|
49
|
+
jsonp: 1,
|
|
50
|
+
post: 2,
|
|
51
|
+
put: 2,
|
|
52
|
+
patch: 2,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Default maximum allowable retries. This can be overridden by setting
|
|
57
|
+
* the `max_retries` property on the wrapped http object.
|
|
58
|
+
*/
|
|
59
|
+
const _default_max_retries = 10;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Initial timeout delay. This dely will be doubled each time to perform
|
|
63
|
+
* an exponential backoff retry protocol. Units in mililiseconds. This
|
|
64
|
+
* can be overridden by setting the `initial_dely` property on the wrapped
|
|
65
|
+
* http object.
|
|
66
|
+
*/
|
|
67
|
+
const _default_initial_delay = 100;
|
|
68
|
+
|
|
69
|
+
export default class HTTPService {
|
|
70
|
+
private static _http401Handler: () => Promise<boolean>;
|
|
71
|
+
private static _onHTTPSuccess: () => void;
|
|
72
|
+
/**
|
|
73
|
+
* A flag to determine whether emrest authorization error has occured
|
|
74
|
+
* as well as to determine the login flow is currently in progress to avoid
|
|
75
|
+
* calling the _http401Handler callback again
|
|
76
|
+
*/
|
|
77
|
+
private static _encountered401Error: boolean = false;
|
|
78
|
+
/**
|
|
79
|
+
* All the calls that were paused because of 401 error are added to this array
|
|
80
|
+
* Once the _encountered401Error is false, all of them will be resolved/restarted
|
|
81
|
+
*/
|
|
82
|
+
private static _authorizationDefers: Array<Deferred<any>> = [];
|
|
83
|
+
|
|
84
|
+
private static _onHttpAuthFlowFn(skipHTTP401Handling: boolean) {
|
|
85
|
+
const defer = ConfigService.q.defer();
|
|
86
|
+
// If _encountered401Error is true then push the defer to _authorizationDefers
|
|
87
|
+
// else just resolve it directly
|
|
88
|
+
if (!skipHTTP401Handling && HTTPService._encountered401Error) {
|
|
89
|
+
HTTPService._authorizationDefers.push(defer);
|
|
90
|
+
} else {
|
|
91
|
+
defer.resolve();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return defer.promise;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Given an object will make sure it's safe for header.
|
|
99
|
+
*/
|
|
100
|
+
private static _encodeHeaderContent(obj: any) {
|
|
101
|
+
return unescape(fixedEncodeURIComponent(JSON.stringify(obj)));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
static setHTTP401Handler(fn: () => Promise<boolean>) {
|
|
105
|
+
HTTPService._http401Handler = fn;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
static setOnHTTPSuccess(fn: () => void) {
|
|
109
|
+
HTTPService._onHTTPSuccess = fn;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Given a respone object from http module, will return the headers
|
|
114
|
+
* This is to ensure angularjs and axios behave the same way.
|
|
115
|
+
* In case of angularjs' $http the headers is a function while for axios
|
|
116
|
+
* it's an object.
|
|
117
|
+
*
|
|
118
|
+
* The response is always going to be an object
|
|
119
|
+
*/
|
|
120
|
+
static getResponseHeader(response: any) {
|
|
121
|
+
if (!response || response.headers === null || response.headers === undefined) return {};
|
|
122
|
+
if (typeof response.headers === 'function') return response.headers();
|
|
123
|
+
if (!('headers' in response)) return {};
|
|
124
|
+
|
|
125
|
+
return response.headers;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
static wrapHTTP(http: any) {
|
|
129
|
+
// wrapping function
|
|
130
|
+
function wrap(method: keyof typeof _method_to_config_idx, fn: any, scope: any) {
|
|
131
|
+
scope = scope || window;
|
|
132
|
+
const cfg_idx = _method_to_config_idx[method];
|
|
133
|
+
return function (this: any, ...args: any) {
|
|
134
|
+
// make sure arguments has a config, and config has a headers
|
|
135
|
+
const config = (args[cfg_idx] = args[cfg_idx] || {});
|
|
136
|
+
|
|
137
|
+
// now add default headers i
|
|
138
|
+
config.headers = config.headers || {};
|
|
139
|
+
|
|
140
|
+
// if no default contextHeaderParams, then just call the fn
|
|
141
|
+
if (this.contextHeaderParams) {
|
|
142
|
+
// Iterate over headers iff they do not collide
|
|
143
|
+
let contextHeader;
|
|
144
|
+
if (typeof config.headers[contextHeaderName] === 'object' && config.headers[contextHeaderName]) {
|
|
145
|
+
contextHeader = config.headers[contextHeaderName];
|
|
146
|
+
} else {
|
|
147
|
+
contextHeader = config.headers[contextHeaderName] = {};
|
|
148
|
+
}
|
|
149
|
+
for (const key in this.contextHeaderParams) {
|
|
150
|
+
if (!(key in contextHeader)) {
|
|
151
|
+
contextHeader[key] = this.contextHeaderParams[key];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* If context header is found in header then encode the stringified value of the header and unescape to keep some of them same
|
|
158
|
+
* JSON and HTTP safe reserved chars: {, }, ", ,, :
|
|
159
|
+
* non-reserved punctuation: -, _, ., ~
|
|
160
|
+
* digit: 0 - 9
|
|
161
|
+
* alpha: A - Z and a - z
|
|
162
|
+
*
|
|
163
|
+
**/
|
|
164
|
+
if (typeof config.headers[contextHeaderName] === 'object') {
|
|
165
|
+
config.headers[contextHeaderName].elapsed_ms = getElapsedTime();
|
|
166
|
+
// encode and make sure it's not very lengthy
|
|
167
|
+
config.headers[contextHeaderName] = HTTPService.certifyContextHeader(config.headers[contextHeaderName]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// now call the fn, with retry logic
|
|
171
|
+
const deferred = ConfigService.q.defer();
|
|
172
|
+
const max_retries = this.max_retries !== undefined && this.max_retries !== null ? this.max_retries : _default_max_retries;
|
|
173
|
+
let delay = this.initial_delay !== undefined && this.initial_delay !== null ? this.initial_delay : _default_initial_delay;
|
|
174
|
+
let count = 0;
|
|
175
|
+
function asyncfn() {
|
|
176
|
+
fn.apply(scope, args).then(
|
|
177
|
+
function (response: any) {
|
|
178
|
+
if (HTTPService._onHTTPSuccess) HTTPService._onHTTPSuccess();
|
|
179
|
+
onload().then(function () {
|
|
180
|
+
deferred.resolve(response);
|
|
181
|
+
});
|
|
182
|
+
},
|
|
183
|
+
function (error: any) {
|
|
184
|
+
/**
|
|
185
|
+
* in axios, network error doesn't have proper status code,
|
|
186
|
+
* so this will make sure we're treating it the same as response.status=-1
|
|
187
|
+
*
|
|
188
|
+
* https://github.com/axios/axios/issues/383
|
|
189
|
+
*/
|
|
190
|
+
if (error.isAxiosError) {
|
|
191
|
+
if (typeof error.response !== 'object') {
|
|
192
|
+
error = { response: { status: _http_status_codes.no_connection } };
|
|
193
|
+
} else if (typeof error.response.status !== 'number') {
|
|
194
|
+
error.response.status = _http_status_codes.no_connection;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* angularjs' $http returns the "response" object
|
|
200
|
+
* while axios returns an "error" object that has "response" in it
|
|
201
|
+
*/
|
|
202
|
+
const response = 'response' in error ? error.response : error;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* there was an error with calling the http module,
|
|
206
|
+
* not that we got an error from server.
|
|
207
|
+
*/
|
|
208
|
+
if (typeof response !== 'object' || response == null) {
|
|
209
|
+
onload().then(function () {
|
|
210
|
+
deferred.reject(error);
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const contentType = HTTPService.getResponseHeader(response)['content-type'];
|
|
216
|
+
|
|
217
|
+
// if retry flag is set, skip on -1 and 0
|
|
218
|
+
const skipRetry = config.skipRetryBrowserError && (response.status == -1 || response.status == 0);
|
|
219
|
+
if (_retriable_error_codes.indexOf(response.status) != -1 && count < max_retries && !skipRetry) {
|
|
220
|
+
count += 1;
|
|
221
|
+
setTimeout(function () {
|
|
222
|
+
HTTPService._onHttpAuthFlowFn(false).then(function () {
|
|
223
|
+
asyncfn();
|
|
224
|
+
});
|
|
225
|
+
}, delay);
|
|
226
|
+
delay *= 2;
|
|
227
|
+
}
|
|
228
|
+
// eslint-disable-next-line eqeqeq
|
|
229
|
+
else if (method === 'delete' && response.status == _http_status_codes.not_found) {
|
|
230
|
+
/** SPECIAL CASE: "retried delete"
|
|
231
|
+
* This indicates that a 'delete' was attempted, but
|
|
232
|
+
* failed due to a transient error. It was retried
|
|
233
|
+
* at least one more time and at some point
|
|
234
|
+
* succeeded.
|
|
235
|
+
*
|
|
236
|
+
* Both of the currently supported delete operations
|
|
237
|
+
* (entity/ and attribute/) return 204 No Content.
|
|
238
|
+
*/
|
|
239
|
+
|
|
240
|
+
// If we get an HTTP error with HTML in it, this means something the server returned as an error.
|
|
241
|
+
// Ermrest never produces HTML errors, so this was produced by the server itself
|
|
242
|
+
if (contentType && contentType.indexOf('html') > -1) {
|
|
243
|
+
response.status = _http_status_codes.internal_server_error;
|
|
244
|
+
// keep response.data the way it is, so client can provide more info to users
|
|
245
|
+
} else {
|
|
246
|
+
response.status = _http_status_codes.no_content;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
onload().then(function () {
|
|
250
|
+
deferred.resolve(response);
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
// eslint-disable-next-line eqeqeq
|
|
254
|
+
else if (response.status == _http_status_codes.unauthorized) {
|
|
255
|
+
// skip the 401 handling
|
|
256
|
+
if (config.skipHTTP401Handling) {
|
|
257
|
+
deferred.reject(response);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// If _encountered401Error is not set then
|
|
262
|
+
if (HTTPService._encountered401Error === false) {
|
|
263
|
+
// If callback has been registered in _http401Handler
|
|
264
|
+
if (typeof HTTPService._http401Handler === 'function') {
|
|
265
|
+
// Set _encountered401Error to avoid the handler from being called again
|
|
266
|
+
HTTPService._encountered401Error = true;
|
|
267
|
+
|
|
268
|
+
// Push the current call to _authroizationDefers by calling _onHttpAuthFlowFn
|
|
269
|
+
HTTPService._onHttpAuthFlowFn(false).then(function () {
|
|
270
|
+
asyncfn();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Call the handler, which will return a promise
|
|
274
|
+
// On success set the flag as false and resolve all the authorizationDefers
|
|
275
|
+
// So that other calls which failed due to 401 or were trigerred after the 401
|
|
276
|
+
// are reexecuted
|
|
277
|
+
// differentUser variable is a boolean variable that states whether the different user logged in after the 401 error was thrown
|
|
278
|
+
HTTPService._http401Handler().then(
|
|
279
|
+
function (differentUser) {
|
|
280
|
+
// if not a different user, continue with retrying previous requests
|
|
281
|
+
// This should handle the case where 'differentUser' is undefined or null as well
|
|
282
|
+
if (!differentUser) {
|
|
283
|
+
HTTPService._encountered401Error = false;
|
|
284
|
+
|
|
285
|
+
HTTPService._authorizationDefers.forEach(function (defer) {
|
|
286
|
+
defer.resolve();
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
function (response) {
|
|
291
|
+
HTTPService._encountered401Error = false;
|
|
292
|
+
deferred.reject(response);
|
|
293
|
+
},
|
|
294
|
+
);
|
|
295
|
+
} else {
|
|
296
|
+
//throw new Error("httpUnauthorizedFn Event Handler not registered");
|
|
297
|
+
deferred.reject(response);
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
// Push the current call to _authroizationDefers by calling _onHttpAuthFlowFn
|
|
301
|
+
HTTPService._onHttpAuthFlowFn(false).then(function () {
|
|
302
|
+
asyncfn();
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
// If we get an HTTP error with HTML in it, this means something the server returned as an error.
|
|
307
|
+
// Ermrest never produces HTML errors, so this was produced by the server itself
|
|
308
|
+
if (contentType && contentType.indexOf('html') > -1) {
|
|
309
|
+
response.status = _http_status_codes.internal_server_error;
|
|
310
|
+
// keep response.data the way it is, so client can provide more info to users
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
onload().then(function () {
|
|
314
|
+
deferred.reject(response);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Push the current call to _authorizationDefers by calling _onHttpAuthFlowFn
|
|
322
|
+
// If the _encountered401Error is false then asyncfn will be called immediately
|
|
323
|
+
// else it will be queued
|
|
324
|
+
HTTPService._onHttpAuthFlowFn(config.skipHTTP401Handling).then(function () {
|
|
325
|
+
asyncfn();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return deferred.promise;
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// now wrap over the supported methods
|
|
333
|
+
const wrapper: any = {};
|
|
334
|
+
for (const method in _method_to_config_idx) {
|
|
335
|
+
wrapper[method] = wrap(method as keyof typeof _method_to_config_idx, http[method], http);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return wrapper;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Given a header object, will encode and if neccessary truncate it.
|
|
343
|
+
* Maximum allowed length of a header after encoding: 6500 characters.
|
|
344
|
+
* The logic is as follows:
|
|
345
|
+
* 1. If the encoded string is not lengthy, return it.
|
|
346
|
+
* 2. otherwise,
|
|
347
|
+
* 2.1. Return an empty object if the minimal header (defined below) goes over the limit.
|
|
348
|
+
* 2.2. Otherwise start truncating `stack` object by doing the following. In each step,
|
|
349
|
+
* if the encoded and truncated header goes below the length limit, return it.
|
|
350
|
+
* - replace all foreign key constraints with their RIDs (if RID is defined for all of them).
|
|
351
|
+
* - replace values (`choices`, `ranges`, `search`) in the filters with the number of values.
|
|
352
|
+
* - replace all `filters.and` with the number of filters.
|
|
353
|
+
* - replace all source paths with the number of path nodes.
|
|
354
|
+
* - use replace stack value with the number of stack nodes.
|
|
355
|
+
* If after performing all these steps, the header is still lengthy, return the minimal header.
|
|
356
|
+
*
|
|
357
|
+
* A minimal header will have the following attributes:
|
|
358
|
+
* - cid, pid, wid, action, schema_table, catalog, t:1
|
|
359
|
+
* And might have these optional attributes:
|
|
360
|
+
* - elapsed_ms, cqp, ppid, pcid
|
|
361
|
+
*
|
|
362
|
+
*/
|
|
363
|
+
static certifyContextHeader(header: any) {
|
|
364
|
+
const MAX_LENGTH = CONTEXT_HEADER_LENGTH_LIMIT;
|
|
365
|
+
|
|
366
|
+
const shorter = _shorterVersion;
|
|
367
|
+
// if RID is not available on even one fk, we will not replacing any of RIDs
|
|
368
|
+
// and go to the next step.
|
|
369
|
+
let noRID = false;
|
|
370
|
+
const replaceConstraintWithRID = function (src: unknown) {
|
|
371
|
+
if (noRID) return false;
|
|
372
|
+
|
|
373
|
+
let fk;
|
|
374
|
+
if (Array.isArray(src)) {
|
|
375
|
+
src.forEach(function (srcNode) {
|
|
376
|
+
if (noRID) return;
|
|
377
|
+
[shorter.outbound, shorter.inbound].forEach(function (direction) {
|
|
378
|
+
if (noRID) return;
|
|
379
|
+
|
|
380
|
+
if (Array.isArray(srcNode[direction])) {
|
|
381
|
+
fk = CatalogService.getConstraintObject(catalog, srcNode[direction][0], srcNode[direction][1]);
|
|
382
|
+
if (fk && fk.RID) {
|
|
383
|
+
srcNode[direction] = fk.RID;
|
|
384
|
+
} else {
|
|
385
|
+
noRID = true;
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return !noRID;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const encode = HTTPService._encodeHeaderContent;
|
|
397
|
+
let res = encode(header);
|
|
398
|
+
|
|
399
|
+
if (res.length < MAX_LENGTH) {
|
|
400
|
+
return res;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const catalog = header.catalog;
|
|
404
|
+
const minimalObj: Record<string, unknown> = {
|
|
405
|
+
cid: header.cid,
|
|
406
|
+
wid: header.wid,
|
|
407
|
+
pid: header.pid,
|
|
408
|
+
catalog: header.catalog,
|
|
409
|
+
schema_table: header.schema_table,
|
|
410
|
+
action: header.action,
|
|
411
|
+
t: 1,
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// these attributes might not be available on the header, but if they
|
|
415
|
+
// are, we must include them in the minimal header content
|
|
416
|
+
['elapsed_ms', 'cqp', 'ppid', 'pcid'].forEach(function (attr) {
|
|
417
|
+
if (attr in header && header[attr]) {
|
|
418
|
+
minimalObj[attr] = header[attr];
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// if even minimal is bigger than the limit, don't log anything
|
|
423
|
+
if (encode(minimalObj).length >= MAX_LENGTH) {
|
|
424
|
+
return {};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// truncation is based on stack, if there's no stack, just log the minimal object
|
|
428
|
+
if (!Array.isArray(header.stack)) {
|
|
429
|
+
return minimalObj;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
let truncated = simpleDeepCopy(header);
|
|
433
|
+
|
|
434
|
+
// replace all fk constraints with their RID
|
|
435
|
+
truncated.stack.forEach(function (stackEl: unknown) {
|
|
436
|
+
if (typeof stackEl !== 'object' || !stackEl) return;
|
|
437
|
+
|
|
438
|
+
// filters
|
|
439
|
+
if (
|
|
440
|
+
'filters' in stackEl &&
|
|
441
|
+
stackEl.filters &&
|
|
442
|
+
typeof stackEl.filters === 'object' &&
|
|
443
|
+
'and' in stackEl.filters &&
|
|
444
|
+
Array.isArray(stackEl.filters.and)
|
|
445
|
+
) {
|
|
446
|
+
stackEl.filters.and.forEach(function (facet) {
|
|
447
|
+
if (noRID) return;
|
|
448
|
+
|
|
449
|
+
if (Array.isArray(facet[shorter.source])) {
|
|
450
|
+
noRID = !replaceConstraintWithRID(facet[shorter.source]);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// sources
|
|
456
|
+
if ('source' in stackEl && stackEl.source && Array.isArray(stackEl.source)) {
|
|
457
|
+
noRID = !replaceConstraintWithRID(stackEl.source);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
if (noRID) {
|
|
462
|
+
truncated = simpleDeepCopy(header);
|
|
463
|
+
} else {
|
|
464
|
+
res = encode(truncated);
|
|
465
|
+
if (res.length < MAX_LENGTH) {
|
|
466
|
+
return res;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// replace choices, ranges, search with number of values
|
|
471
|
+
truncated.stack.forEach(function (stackEl: unknown) {
|
|
472
|
+
if (typeof stackEl !== 'object' || !stackEl) return;
|
|
473
|
+
if (
|
|
474
|
+
'filters' in stackEl &&
|
|
475
|
+
stackEl.filters &&
|
|
476
|
+
typeof stackEl.filters === 'object' &&
|
|
477
|
+
'and' in stackEl.filters &&
|
|
478
|
+
Array.isArray(stackEl.filters.and)
|
|
479
|
+
) {
|
|
480
|
+
stackEl.filters!.and.forEach(function (facet: any) {
|
|
481
|
+
[shorter.choices, shorter.ranges, shorter.search].forEach(function (k) {
|
|
482
|
+
facet[k] = Array.isArray(facet[k]) ? facet[k].length : 1;
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
res = encode(truncated);
|
|
489
|
+
if (res.length < MAX_LENGTH) {
|
|
490
|
+
return res;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// replace all filters.and with the number of filters
|
|
494
|
+
truncated.stack.forEach(function (stackEl: unknown) {
|
|
495
|
+
if (typeof stackEl !== 'object' || !stackEl) return;
|
|
496
|
+
if (
|
|
497
|
+
'filters' in stackEl &&
|
|
498
|
+
stackEl.filters &&
|
|
499
|
+
typeof stackEl.filters === 'object' &&
|
|
500
|
+
'and' in stackEl.filters &&
|
|
501
|
+
Array.isArray(stackEl.filters.and)
|
|
502
|
+
) {
|
|
503
|
+
stackEl.filters.and = stackEl.filters.and.length;
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
res = encode(truncated);
|
|
508
|
+
if (res.length < MAX_LENGTH) {
|
|
509
|
+
return res;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// replace all source paths with the number of path nodes
|
|
513
|
+
truncated.stack.forEach(function (stackEl: unknown) {
|
|
514
|
+
if (typeof stackEl !== 'object' || !stackEl) return;
|
|
515
|
+
if ('source' in stackEl && stackEl.source) {
|
|
516
|
+
stackEl.source = Array.isArray(stackEl.source) ? stackEl.source.length : 1;
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
res = encode(truncated);
|
|
521
|
+
if (res.length < MAX_LENGTH) {
|
|
522
|
+
return res;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// replace stack with the number of elements
|
|
526
|
+
truncated.stack = truncated.stack.length;
|
|
527
|
+
|
|
528
|
+
res = encode(truncated);
|
|
529
|
+
if (res.length < MAX_LENGTH) {
|
|
530
|
+
return res;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// if none of the truncation works, just return the minimal obj
|
|
534
|
+
return encode(minimalObj);
|
|
535
|
+
}
|
|
536
|
+
}
|