@isrd-isi-edu/ermrestjs 2.0.0

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