@gisatcz/ptr-be-core 0.0.1-dev.6 → 0.0.1-dev.8

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.
@@ -1,7 +1,10 @@
1
1
  'use strict';
2
2
 
3
+ var luxon = require('luxon');
3
4
  var pino = require('pino');
4
5
  var pretty = require('pino-pretty');
6
+ var crypto = require('crypto');
7
+ var lodash = require('lodash');
5
8
 
6
9
  /**
7
10
  * Check if the value in included in enum posibilities.
@@ -98,6 +101,206 @@ const flattenObject = (obj, prefix = '') => {
98
101
  }, {});
99
102
  };
100
103
 
104
+ /**
105
+ * Extract message from exception error (try-catch)
106
+ * @param error error from catch block as any
107
+ * @returns
108
+ */
109
+ const messageFromError = (error) => error["message"];
110
+ /**
111
+ * We miss a API parameter needed to process action
112
+ */
113
+ class InvalidRequestError extends Error {
114
+ constructor(message) {
115
+ super(`Invalid Request: ${message}`);
116
+ }
117
+ }
118
+ /**
119
+ * Where client has general authorization issue
120
+ */
121
+ class AuthorizationError extends Error {
122
+ constructor() {
123
+ super(`Authorization has failed.`);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Return epoch timestamp
129
+ * @param regime Set if you want milisecond format or second format
130
+ * @returns
131
+ */
132
+ const nowTimestamp = (regime = "milisecond") => {
133
+ const timestamp = luxon.DateTime.now().toMillis();
134
+ return regime === "second" ? Math.round((timestamp / 1000)) : timestamp;
135
+ };
136
+ /**
137
+ * Convert epoch time value into ISO format
138
+ * @param epochValue Epoch value of the timestamp
139
+ * @returns ISO format of the date
140
+ */
141
+ const epochToIsoFormat = (epochValue) => luxon.DateTime.fromMillis(epochValue).toISO();
142
+ /**
143
+ * Return epoch timestamp
144
+ * @param regime Set if you want milisecond format or second format
145
+ * @returns
146
+ */
147
+ const nowTimestampIso = () => {
148
+ const timestamp = luxon.DateTime.now().toISO();
149
+ return timestamp;
150
+ };
151
+ /**
152
+ * Check if input date is valid for ISO format
153
+ * @param dateToCheck
154
+ * @returns
155
+ */
156
+ const hasIsoFormat = (dateToCheck) => {
157
+ try {
158
+ const toDate = new Date(Date.parse(dateToCheck));
159
+ const isoCheck = toDate.toISOString().includes(dateToCheck);
160
+ return isoCheck;
161
+ }
162
+ catch {
163
+ return false;
164
+ }
165
+ };
166
+ /**
167
+ * Convert date in ISO formtat to milisecond timestamp
168
+ * @param isoDate Date in ISO 8601 format
169
+ * @returns Timestamp representing the date in miliseconds
170
+ */
171
+ const isoDateToTimestamp = (isoDate) => luxon.DateTime.fromISO(isoDate).toMillis();
172
+ /**
173
+ * Format ISO 8601 interval to from-to values
174
+ * @param interval Defined inteval in ISO format (from/to) of the UTC
175
+ * @returns Tuple - from timestamp and to timestamp
176
+ */
177
+ const isoIntervalToTimestamps = (interval) => {
178
+ // Split the interval into two parts
179
+ const intervals = interval.split("/");
180
+ // interval as a single year has just one part
181
+ if (intervals.length == 1) {
182
+ const newIso = `${interval}-01-01/${interval}-12-31`;
183
+ return isoIntervalToTimestamps(newIso);
184
+ }
185
+ // interval with two parts or less than one
186
+ else if (intervals.length > 2 || intervals.length < 1)
187
+ throw new InvalidRequestError("Interval can have only two parameters");
188
+ // valid interval with two parts
189
+ else {
190
+ if (!intervals.every(interval => hasIsoFormat(interval)))
191
+ throw new InvalidRequestError("Parameter utcIntervalIso is not ISO 8601 time interval (date01/date02) or year");
192
+ const [int1, int2] = intervals.map(intervalIso => {
193
+ const cleared = intervalIso.replace(" ", "");
194
+ return isoDateToTimestamp(cleared);
195
+ });
196
+ return [int1, int2];
197
+ }
198
+ };
199
+
200
+ // TODO: Cover by tests
201
+ // TODO: mode to ptr-be-core as general CSV methods
202
+ /**
203
+ * Parses a single line of CSV-formatted strings into an array of trimmed string values.
204
+ *
205
+ * @param csvStingsLine - A string representing a single line of comma-separated values.
206
+ * @returns An array of strings, each representing a trimmed value from the CSV line.
207
+ */
208
+ const csvParseStrings = (csvStingsLine) => {
209
+ return csvStingsLine.split(",").map((value) => value.trim());
210
+ };
211
+ /**
212
+ * Parses a comma-separated string of numbers into an array of numbers.
213
+ *
214
+ * @param csvNumbersLine - A string containing numbers separated by commas (e.g., "1, 2, 3.5").
215
+ * @returns An array of numbers parsed from the input string.
216
+ */
217
+ const csvParseNumbers = (csvNumbersLine) => {
218
+ return csvNumbersLine.split(",").map((value) => parseFloat(value.trim()));
219
+ };
220
+
221
+ /**
222
+ * Finds the first node in the provided list that contains the specified label.
223
+ *
224
+ * Searches the given array of FullPantherEntity objects and returns the first entity
225
+ * whose `labels` array includes the provided label.
226
+ *
227
+ * @param nodes - Array of FullPantherEntity objects to search through.
228
+ * @param label - Label to match; may be a UsedDatasourceLabels or UsedNodeLabels.
229
+ * @returns The first FullPantherEntity whose `labels` includes `label`, or `undefined`
230
+ * if no such entity is found.
231
+ *
232
+ * @remarks
233
+ * - The search stops at the first match (uses `Array.prototype.find`).
234
+ * - Label comparison is exact (uses `Array.prototype.includes`), so it is case-sensitive
235
+ * and requires the same string instance/value.
236
+ *
237
+ * @example
238
+ * const result = findNodeByLabel(nodes, 'datasource-main');
239
+ * if (result) {
240
+ * // found a node that has the 'datasource-main' label
241
+ * }
242
+ */
243
+ const findNodeByLabel = (nodes, label) => {
244
+ return nodes.find(n => n.labels.includes(label));
245
+ };
246
+ /**
247
+ * Filters an array of FullPantherEntity objects, returning only those that contain the specified label.
248
+ *
249
+ * The function performs a shallow, non-mutating filter: it returns a new array and does not modify the input.
250
+ * Matching is done using Array.prototype.includes on each entity's `labels` array (strict equality).
251
+ *
252
+ * @param nodes - The array of entities to filter.
253
+ * @param label - The label to match; can be a UsedDatasourceLabels or UsedNodeLabels value.
254
+ * @returns A new array containing only the entities whose `labels` array includes the provided label.
255
+ *
256
+ * @remarks
257
+ * Time complexity is O(n * m) where n is the number of entities and m is the average number of labels per entity.
258
+ *
259
+ * @example
260
+ * ```ts
261
+ * const matched = filterNodeByLabel(entities, 'MY_LABEL');
262
+ * ```
263
+ */
264
+ const filterNodeByLabel = (nodes, label) => {
265
+ return nodes.filter(n => n.labels.includes(label));
266
+ };
267
+ /**
268
+ * Finds the first edge in the provided array whose label strictly equals the given label.
269
+ *
270
+ * @param edges - Array of GraphEdge objects to search.
271
+ * @param label - The UsedEdgeLabels value to match against each edge's `label` property.
272
+ * @returns The first matching GraphEdge if found; otherwise `undefined`.
273
+ *
274
+ * @example
275
+ * const edge = findEdgeByLabel(edges, 'dependency');
276
+ * if (edge) {
277
+ * // handle found edge
278
+ * }
279
+ */
280
+ const findEdgeByLabel = (edges, label) => {
281
+ return edges.find(e => e.label === label);
282
+ };
283
+ /**
284
+ * Filters a list of GraphEdge objects by a specific edge label.
285
+ *
286
+ * Returns a new array containing only those edges whose `label` property
287
+ * strictly equals the provided `label` argument. The original `edges`
288
+ * array is not mutated.
289
+ *
290
+ * @param edges - Array of GraphEdge objects to filter.
291
+ * @param label - The label to match; comparison is performed using strict (`===`) equality.
292
+ * @returns A new array of GraphEdge objects whose `label` matches the provided label. Returns an empty array if no edges match.
293
+ *
294
+ * @remarks
295
+ * Time complexity: O(n), where n is the number of edges.
296
+ *
297
+ * @example
298
+ * // const result = filterEdgeByLabel(edges, 'CONNECTS');
299
+ */
300
+ const filterEdgeByLabel = (edges, label) => {
301
+ return edges.filter(e => e.label === label);
302
+ };
303
+
101
304
  /**
102
305
  * What types of graph nodes we use in metadata model
103
306
  */
@@ -131,7 +334,9 @@ exports.UsedDatasourceLabels = void 0;
131
334
  UsedDatasourceLabels["PostGIS"] = "postgis";
132
335
  UsedDatasourceLabels["WMTS"] = "wmts";
133
336
  UsedDatasourceLabels["WFS"] = "wfs";
134
- UsedDatasourceLabels["GeoPackage"] = "geopackage"; // OGC GeoPackage format
337
+ UsedDatasourceLabels["GeoPackage"] = "geopackage";
338
+ UsedDatasourceLabels["MapStyle"] = "mapStyle";
339
+ UsedDatasourceLabels["Timeseries"] = "timeseries"; // Timeseries datasource (with from-to and step)
135
340
  })(exports.UsedDatasourceLabels || (exports.UsedDatasourceLabels = {}));
136
341
  /**
137
342
  * What types of edges we use in metadata model
@@ -142,29 +347,17 @@ exports.UsedEdgeLabels = void 0;
142
347
  UsedEdgeLabels["Has"] = "HAS";
143
348
  UsedEdgeLabels["InPostgisLocation"] = "IN_POSTGIS_LOCATION"; // Edge to connect datasource with PostGIS location (schema, table, geometry column)
144
349
  })(exports.UsedEdgeLabels || (exports.UsedEdgeLabels = {}));
145
-
146
- /**
147
- * Extract message from exception error (try-catch)
148
- * @param error error from catch block as any
149
- * @returns
150
- */
151
- const messageFromError = (error) => error["message"];
152
350
  /**
153
- * We miss a API parameter needed to process action
351
+ * What time steps are used in timeseries data
154
352
  */
155
- class InvalidRequestError extends Error {
156
- constructor(message) {
157
- super(`Invalid Request: ${message}`);
158
- }
159
- }
160
- /**
161
- * Where client has general authorization issue
162
- */
163
- class AuthorizationError extends Error {
164
- constructor() {
165
- super(`Authorization has failed.`);
166
- }
167
- }
353
+ exports.UsedTimeseriesSteps = void 0;
354
+ (function (UsedTimeseriesSteps) {
355
+ UsedTimeseriesSteps["Year"] = "year";
356
+ UsedTimeseriesSteps["Quarter"] = "quarter";
357
+ UsedTimeseriesSteps["Month"] = "month";
358
+ UsedTimeseriesSteps["Week"] = "week";
359
+ UsedTimeseriesSteps["Day"] = "day";
360
+ })(exports.UsedTimeseriesSteps || (exports.UsedTimeseriesSteps = {}));
168
361
 
169
362
  // logger.ts
170
363
  const DEFAULT_LOG_OPTIONS = {
@@ -193,17 +386,553 @@ class AppLogger {
193
386
  }
194
387
  }
195
388
 
389
+ /**
390
+ * Validates the provided labels to ensure they are an array of strings and that each label
391
+ * is a valid value within the specified enums (`UsedNodeLabels` or `UsedDatasourceLabels`).
392
+ *
393
+ * @param labels - The input to validate, expected to be an array of strings.
394
+ * @throws {InvalidRequestError} If `labels` is not an array.
395
+ * @throws {InvalidRequestError} If any label in the array is not a valid value in the combined enums.
396
+ */
397
+ const validateNodeLabels = (labels) => {
398
+ if (labels === undefined || labels === null) {
399
+ throw new InvalidRequestError("Graph node labels are required.");
400
+ }
401
+ if (!lodash.isArray(labels))
402
+ throw new InvalidRequestError(`Graph node labels must be an array of strings.`);
403
+ if (labels.length === 0)
404
+ throw new InvalidRequestError("Graph node labels array must contain at least one label.");
405
+ for (const label of labels) {
406
+ if (!isInEnum(label, exports.UsedNodeLabels) && !isInEnum(label, exports.UsedDatasourceLabels))
407
+ throw new InvalidRequestError(`Label ${label} is not supported. Value must be one of: ${enumCombineValuesToString([exports.UsedNodeLabels, exports.UsedDatasourceLabels])}`);
408
+ }
409
+ };
410
+ /**
411
+ * Validate a graph edge label.
412
+ *
413
+ * The value is first checked for presence (not `undefined`/`null`), then for type (`string`).
414
+ * The string is normalised using `toLocaleLowerCase()` and validated against the `UsedEdgeLabels` enum.
415
+ *
416
+ * @param label - The value to validate; expected to be a string representing an edge label.
417
+ *
418
+ * @throws {InvalidRequestError} If `label` is `undefined` or `null`.
419
+ * @throws {InvalidRequestError} If `label` is not a `string`.
420
+ * @throws {InvalidRequestError} If the normalised label is not one of the supported values in `UsedEdgeLabels`.
421
+ *
422
+ * @example
423
+ * validateEdgeLabel('CONNECTS'); // succeeds if 'connects' exists in UsedEdgeLabels
424
+ *
425
+ * @remarks
426
+ * Membership is determined via `isInEnum` and error messages include the allowed values (via `enumCombineValuesToString`).
427
+ */
428
+ const validateEdgeLabel = (label) => {
429
+ if (label === undefined || label === null) {
430
+ throw new InvalidRequestError("Graph edge label is required.");
431
+ }
432
+ if (typeof label !== "string") {
433
+ throw new InvalidRequestError(`Graph edge label must be a string.`);
434
+ }
435
+ const normalisedLabel = label.toLocaleLowerCase();
436
+ if (!isInEnum(normalisedLabel, exports.UsedEdgeLabels)) {
437
+ throw new InvalidRequestError(`Graph edge label '${normalisedLabel}' is not supported. Value must be one of: ${enumCombineValuesToString([exports.UsedEdgeLabels])}`);
438
+ }
439
+ };
440
+
441
+ /**
442
+ * Extract and parse basic entitry from request body
443
+ * This parse function is used in other parsers, becuase basic entity is part of other models
444
+ * @param bodyRaw Body from http request
445
+ * @param key Optional - key value for existing recods in database
446
+ * @returns Parsed entity - all unwanted parameters are gone
447
+ */
448
+ const parseBasicNodeFromBody = (bodyRaw) => {
449
+ const { labels, nameInternal, nameDisplay, description, key } = bodyRaw;
450
+ validateNodeLabels(labels);
451
+ const basicGraphResult = {
452
+ lastUpdatedAt: nowTimestamp(),
453
+ key: key ?? crypto.randomUUID(),
454
+ nameInternal: nameInternal ?? "",
455
+ nameDisplay: nameDisplay ?? "",
456
+ description: description ?? "",
457
+ labels: labels
458
+ };
459
+ return basicGraphResult;
460
+ };
461
+ /**
462
+ * Parse node of type area tree level
463
+ * @param levelBody Content from request
464
+ * @returns Parsed area tree level
465
+ */
466
+ const paseHasLevels = (levelBody) => {
467
+ const { level } = levelBody;
468
+ const result = {
469
+ level
470
+ };
471
+ return result;
472
+ };
473
+ /**
474
+ * Parse body to period entity
475
+ * @param bodyRaw Request body content - can be anything
476
+ * @returns Parsed entity - all unwanted parameters are gone
477
+ */
478
+ const parseWithInterval = (bodyRaw) => {
479
+ const { intervalISO, } = bodyRaw;
480
+ if (!intervalISO)
481
+ throw new InvalidRequestError("Period must have UTC interval in ISO format");
482
+ const [from, to] = isoIntervalToTimestamps(intervalISO);
483
+ const intervalResult = {
484
+ validIntervalIso: intervalISO,
485
+ validFrom: from,
486
+ validTo: to
487
+ };
488
+ return intervalResult;
489
+ };
490
+ const parseWithConfiguration = (bodyRaw, required = false) => {
491
+ const { configuration } = bodyRaw;
492
+ if (!configuration && required)
493
+ throw new InvalidRequestError("Configuration is required");
494
+ if (!configuration)
495
+ return;
496
+ return { configuration: typeof configuration === 'string' ? configuration : JSON.stringify(configuration) };
497
+ };
498
+ /**
499
+ * Parse body to place entity
500
+ * @param bodyRaw Request body content - can be anything
501
+ * @returns
502
+ */
503
+ const parseHasGeometry = (bodyRaw) => {
504
+ const { bbox, geometry, } = bodyRaw;
505
+ /**
506
+ * Convert bbox from CSV string to array of 4 coordinates
507
+ * @returns Parsed bounding box from CSV string
508
+ */
509
+ const bboxFromCSV = () => {
510
+ const bboxFromCSV = csvParseNumbers(bbox);
511
+ if (bboxFromCSV.length !== 4)
512
+ throw new InvalidRequestError("bbox must be an array of 4 numbers");
513
+ return bboxFromCSV;
514
+ };
515
+ const geometryResult = {
516
+ bbox: bbox ? bboxFromCSV() : [],
517
+ geometry: geometry ?? ""
518
+ };
519
+ return geometryResult;
520
+ };
521
+ /**
522
+ * Parses the input object and extracts the `url` property.
523
+ * If the `url` property is not present or is undefined, it returns `null` for `url`.
524
+ *
525
+ * @param bodyRaw - The raw input object that may contain a `url` property.
526
+ * @returns An object with a single `url` property, which is either the extracted value or `null`.
527
+ */
528
+ const parseHasUrl = (bodyRaw, isRequired = true) => {
529
+ const { url } = bodyRaw;
530
+ if (isRequired && !url)
531
+ throw new InvalidRequestError("Url is required for the node");
532
+ if (!url)
533
+ return;
534
+ return { url };
535
+ };
536
+ /**
537
+ * Parses the `specificName` property from the provided object and returns it wrapped in a `HasSpecificName` type.
538
+ *
539
+ * @param bodyRaw - The raw input object potentially containing the `specificName` property.
540
+ * @param isRequired - If `true`, throws an `InvalidRequestError` when `specificName` is missing. Defaults to `false`.
541
+ * @returns An object with the `specificName` property if present, or `undefined` if not required and missing.
542
+ * @throws {InvalidRequestError} If `isRequired` is `true` and `specificName` is not provided.
543
+ */
544
+ const parseHasSpecificName = (bodyRaw, isRequired = false) => {
545
+ const { specificName } = bodyRaw;
546
+ if (isRequired && !specificName)
547
+ throw new InvalidRequestError("Property specificName is required for the node");
548
+ if (!specificName)
549
+ return;
550
+ return { specificName };
551
+ };
552
+ /**
553
+ * Parses the `color` property from the provided `bodyRaw` object and returns it
554
+ * wrapped in an object if it exists. If the `isRequired` flag is set to `true`
555
+ * and the `color` property is missing, an error is thrown.
556
+ *
557
+ * @param bodyRaw - The raw input object containing the `color` property.
558
+ * @param isRequired - A boolean indicating whether the `color` property is required.
559
+ * Defaults to `false`.
560
+ * @returns An object containing the `color` property if it exists, or `undefined` if not required.
561
+ * @throws {InvalidRequestError} If `isRequired` is `true` and the `color` property is missing.
562
+ */
563
+ const parseWithColor = (bodyRaw, isRequired = false) => {
564
+ const { color } = bodyRaw;
565
+ if (isRequired && !color)
566
+ throw new InvalidRequestError("Property color is required for the node");
567
+ if (!color)
568
+ return;
569
+ return { color };
570
+ };
571
+ /**
572
+ * Parses the provided raw body object to extract unit and valueType properties.
573
+ * Ensures that the required properties are present if `isRequired` is set to true.
574
+ *
575
+ * @param bodyRaw - The raw input object containing the properties to parse.
576
+ * @param isRequired - A boolean indicating whether the `unit` and `valueType` properties are mandatory.
577
+ * Defaults to `false`.
578
+ * @returns An object containing the `unit` and `valueType` properties, or `null` if they are not provided.
579
+ * @throws {InvalidRequestError} If `isRequired` is true and either `unit` or `valueType` is missing.
580
+ */
581
+ const parseWithUnits = (bodyRaw, isRequired = false) => {
582
+ const { unit, valueType } = bodyRaw;
583
+ if (isRequired && (!unit || !valueType))
584
+ throw new InvalidRequestError("Properties unit and valueType are required for the node");
585
+ return { unit: unit ?? null, valueType: valueType ?? null };
586
+ };
587
+ /**
588
+ * Parse and validate the `documentId` property from a raw request body.
589
+ *
590
+ * Extracts `documentId` from `bodyRaw` and returns it as an object matching
591
+ * HasDocumentId, wrapped in the Unsure type. If `documentId` is missing or
592
+ * falsy, the function throws an InvalidRequestError.
593
+ *
594
+ * @param bodyRaw - The raw request body (e.g. parsed JSON) expected to contain `documentId`.
595
+ * @returns Unsure<HasDocumentId> — an object with the `documentId` property.
596
+ * @throws {InvalidRequestError} Thrown when `documentId` is not present on `bodyRaw`.
597
+ */
598
+ const parseHasDocumentId = (bodyRaw) => {
599
+ const { documentId } = bodyRaw;
600
+ if (!documentId)
601
+ throw new InvalidRequestError("Property documentId is required for the node");
602
+ return { documentId };
603
+ };
604
+ /**
605
+ * Parse timeseries information from a raw request body.
606
+ *
607
+ * Delegates interval parsing to `parseWithInterval(bodyRaw)` and then
608
+ * attaches the `step` value from the provided `bodyRaw` to the resulting
609
+ * timeseries object. The function does not mutate the input object.
610
+ *
611
+ * @param bodyRaw - Raw request payload (unknown/loose shape). Expected to
612
+ * contain whatever fields `parseWithInterval` requires and
613
+ * optionally a `step` property.
614
+ * @returns A `HasTimeseries` object composed of the interval-related fields
615
+ * returned by `parseWithInterval` plus the `step` property from
616
+ * `bodyRaw` (which may be `undefined` if not present).
617
+ * @throws Rethrows any errors produced by `parseWithInterval` when the input
618
+ * body is invalid for interval parsing.
619
+ */
620
+ const parseWithTimeseries = (bodyRaw) => {
621
+ const timeseriesIntervals = parseWithInterval(bodyRaw);
622
+ const { step } = bodyRaw;
623
+ if (!step)
624
+ throw new InvalidRequestError("Property step is required for timeseries datasource");
625
+ return { ...timeseriesIntervals, step };
626
+ };
627
+ /**
628
+ * Parses the `bands`, `bandNames`, and `bandPeriods` properties from the provided raw input object.
629
+ *
630
+ * - If `required` is `true`, throws an `InvalidRequestError` if any of the properties are missing.
631
+ * - Converts CSV strings to arrays:
632
+ * - `bands` is parsed as an array of numbers.
633
+ * - `bandNames` and `bandPeriods` are parsed as arrays of trimmed strings.
634
+ * - Returns an object containing any of the parsed properties that were present in the input.
635
+ *
636
+ * @param bodyRaw - The raw input object potentially containing `bands`, `bandNames`, and `bandPeriods` as CSV strings.
637
+ * @param required - If `true`, all three properties are required and an error is thrown if any are missing. Defaults to `false`.
638
+ * @returns An object with the parsed properties, or `undefined` if none are present.
639
+ * @throws {InvalidRequestError} If `required` is `true` and any property is missing.
640
+ */
641
+ const parseHasBands = (bodyRaw, required = false) => {
642
+ const { bands, bandNames, bandPeriods } = bodyRaw;
643
+ let result;
644
+ if (required && (!bands || !bandNames || !bandPeriods))
645
+ throw new InvalidRequestError("Bands, bandNames and bandPeriods are required for the node");
646
+ if (bands) {
647
+ result = result ?? {};
648
+ Object.assign(result, { bands: csvParseNumbers(bands) });
649
+ }
650
+ if (bandNames) {
651
+ result = result ?? {};
652
+ Object.assign(result, { bandNames: csvParseStrings(bandNames) });
653
+ }
654
+ if (bandPeriods) {
655
+ result = result ?? {};
656
+ Object.assign(result, { bandPeriods: csvParseStrings(bandPeriods) });
657
+ }
658
+ return result;
659
+ };
660
+ /**
661
+ * Parse single graph node from body entity
662
+ * @param bodyNodeEntity Entity from request body
663
+ * @returns Parsed object for specific node
664
+ */
665
+ const parseSinglePantherNode = (bodyNodeEntity) => {
666
+ // Parse basic node properties first
667
+ let node = parseBasicNodeFromBody(bodyNodeEntity);
668
+ // Parse additional properties for specific node types
669
+ // single for loop is used to avoid multiple labels array iterations
670
+ for (const label of node.labels) {
671
+ // If node is a Period, add interval information
672
+ if (label === exports.UsedNodeLabels.Period)
673
+ node = { ...node, ...parseWithInterval(bodyNodeEntity) };
674
+ // If node is a Place, add geographic information
675
+ if (label === exports.UsedNodeLabels.Place)
676
+ node = { ...node, ...parseHasGeometry(bodyNodeEntity) };
677
+ // If node is a Datasource or Application, add configuration when available
678
+ if (label === exports.UsedNodeLabels.Datasource || label === exports.UsedNodeLabels.Application) {
679
+ const parsedConfiguration = parseWithConfiguration(bodyNodeEntity, false);
680
+ node = parsedConfiguration ? { ...node, ...parsedConfiguration } : node;
681
+ }
682
+ // If node is a online Datasource, add URL information
683
+ const datasourcesWithUrl = [
684
+ exports.UsedDatasourceLabels.COG,
685
+ exports.UsedDatasourceLabels.WMS,
686
+ exports.UsedDatasourceLabels.MVT,
687
+ exports.UsedDatasourceLabels.WFS,
688
+ exports.UsedDatasourceLabels.WMTS,
689
+ exports.UsedDatasourceLabels.Geojson
690
+ ];
691
+ // If node is a Datasource with URL, add URL information
692
+ if (datasourcesWithUrl.includes(label)) {
693
+ const parsedUrl = parseHasUrl(bodyNodeEntity, true);
694
+ node = parsedUrl ? { ...node, ...parsedUrl } : node;
695
+ }
696
+ // If node is a Datasource with bands, add bands information
697
+ const datasourcesWithPossibleBands = [
698
+ exports.UsedDatasourceLabels.COG,
699
+ ];
700
+ // If node is a Datasource can have bands, add them
701
+ if (datasourcesWithPossibleBands.includes(label)) {
702
+ const parsedBands = parseHasBands(bodyNodeEntity, false);
703
+ node = parsedBands ? { ...node, ...parsedBands } : node;
704
+ }
705
+ // If node is a Style, add specific name information
706
+ if (label === exports.UsedNodeLabels.Style || label === exports.UsedDatasourceLabels.MapStyle) {
707
+ const parsedSpecificName = parseHasSpecificName(bodyNodeEntity, true);
708
+ node = parsedSpecificName ? { ...node, ...parsedSpecificName } : node;
709
+ }
710
+ // If node is a Datasource with timeseries, add timeseries information
711
+ if (label === exports.UsedDatasourceLabels.Timeseries) {
712
+ const parsedTimeseries = parseWithTimeseries(bodyNodeEntity);
713
+ node = { ...node, ...parsedTimeseries };
714
+ }
715
+ // If node is a Datasource with document ID, add document ID information
716
+ const datasourcesWithDocumentId = [
717
+ exports.UsedDatasourceLabels.PostGIS,
718
+ exports.UsedDatasourceLabels.Timeseries
719
+ ];
720
+ if (datasourcesWithDocumentId.includes(label)) {
721
+ const parsedDocumentId = parseHasDocumentId(bodyNodeEntity);
722
+ node = { ...node, ...parsedDocumentId };
723
+ }
724
+ // If node is an AreaTreeLevel, add level information
725
+ if (label === exports.UsedNodeLabels.AreaTreeLevel)
726
+ node = { ...node, ...paseHasLevels(bodyNodeEntity) };
727
+ // If node is an Attribute, add color information and units
728
+ if (label === exports.UsedNodeLabels.Attribute) {
729
+ const parsedColor = parseWithColor(bodyNodeEntity, false);
730
+ const parsedUnit = parseWithUnits(bodyNodeEntity, false);
731
+ // Add parsed unit if available
732
+ node = parsedUnit ? {
733
+ ...node,
734
+ ...parsedUnit
735
+ } : node;
736
+ // Add parsed color if available
737
+ node = parsedColor ? {
738
+ ...node,
739
+ ...parsedColor
740
+ } : node;
741
+ }
742
+ }
743
+ return node;
744
+ };
745
+ /**
746
+ * Parse array of graph nodes from request body
747
+ * @param body Array of graph nodes inside http request body
748
+ * @returns Array of parsed graph nodes in correct form
749
+ */
750
+ const parseParsePantherNodes = (body) => {
751
+ const nodeArray = body;
752
+ if (!lodash.isArray(nodeArray))
753
+ throw new InvalidRequestError("Request: Grah nodes must be an array");
754
+ return nodeArray.map(PantherEntity => parseSinglePantherNode(PantherEntity));
755
+ };
756
+ // TODO: cover by better testing
757
+
758
+ /**
759
+ * Parses a node object from the Arrows JSON format and converts it into a `PantherEntity`.
760
+ *
761
+ * Validates that the node's labels are arrays of supported enum values, and constructs
762
+ * a `PantherEntity` object with the appropriate properties. Throws an `InvalidRequestError`
763
+ * if the labels are not valid.
764
+ *
765
+ * @param node - The node object from the Arrows JSON to parse.
766
+ * @returns A `PantherEntity` object representing the parsed node.
767
+ * @throws {InvalidRequestError} If the node's labels are not an array of supported values.
768
+ */
769
+ const parseNodeFromArrows = (node) => {
770
+ const { labels, properties, id, caption } = node;
771
+ validateNodeLabels(labels);
772
+ const basicGraphResult = parseSinglePantherNode({
773
+ labels: labels,
774
+ key: properties.key ?? id ?? crypto.randomUUID(),
775
+ nameDisplay: caption ?? properties.nameDisplay ?? "",
776
+ ...properties
777
+ });
778
+ return basicGraphResult;
779
+ };
780
+ /**
781
+ * Parses an edge object from the Arrows format and converts it into a `GraphEdge`.
782
+ *
783
+ * @param edge - The edge object to parse, containing details about the graph edge.
784
+ * @returns A `GraphEdge` object containing the parsed edge nodes, label, and properties.
785
+ *
786
+ * @throws {InvalidRequestError} If the edge does not have a type (`label`).
787
+ * @throws {InvalidRequestError} If the edge type (`label`) is not supported.
788
+ * @throws {InvalidRequestError} If the edge does not have properties.
789
+ * @throws {InvalidRequestError} If the edge does not have `fromId` or `toId`.
790
+ * @throws {InvalidRequestError} If the `fromId` and `toId` are the same.
791
+ */
792
+ const parseEdgeFromArrows = (edge) => {
793
+ const { fromId: from, toId: to, type, properties } = edge;
794
+ // Determine the label, defaulting to "RelatedTo" if the type is invalid or not provided
795
+ const label = (typeof type === "string" && type.trim().length > 0)
796
+ ? type.trim()
797
+ : exports.UsedEdgeLabels.RelatedTo;
798
+ // validate the edge properties
799
+ if (!isInEnum(label, exports.UsedEdgeLabels))
800
+ throw new InvalidRequestError(`Graph edge type '${label}' is not supported`);
801
+ // edge must have a properties
802
+ if (!properties)
803
+ throw new InvalidRequestError(`Graph edge must have properties`);
804
+ // edge must have fromId and toId
805
+ if (!from || !to)
806
+ throw new InvalidRequestError(`Graph edge must have fromId and toId`);
807
+ if (from === to)
808
+ throw new InvalidRequestError(`Cannot connect two same keys in graph relation (${from} leads to ${to})`);
809
+ // prepare relation tuple
810
+ const result = [from, to];
811
+ // construct the parsed edge object
812
+ const parsedEdge = {
813
+ edgeNodes: result,
814
+ label,
815
+ properties
816
+ };
817
+ // return the parsed edge
818
+ return parsedEdge;
819
+ };
820
+ /**
821
+ * Parses a JSON object representing nodes and relationships (edges) from the Arrows JSON.
822
+ *
823
+ * @param body - The input JSON object to parse. Expected to contain `nodes` and `relationships` arrays.
824
+ * @returns An object containing parsed `nodes` and `edges` arrays.
825
+ * @throws {InvalidRequestError} If the input is not a valid object, or if required properties are missing or not arrays.
826
+ */
827
+ const parseArrowsJson = (body) => {
828
+ // Check if the body is a valid object
829
+ if (typeof body !== "object" || body === null)
830
+ throw new InvalidRequestError("Invalid JSON format");
831
+ // Check if the body contains the required properties
832
+ if (!("nodes" in body) || !("relationships" in body))
833
+ throw new InvalidRequestError("Invalid JSON format: Missing nodes or relationships");
834
+ // Check if nodes and relationships are arrays
835
+ if (!lodash.isArray(body.nodes) || !lodash.isArray(body.relationships))
836
+ throw new InvalidRequestError("Invalid JSON format: nodes and relationships must be arrays");
837
+ // Extract nodes and relationships from the body
838
+ const { nodes: rawNodes, relationships: rawEdges } = body;
839
+ // Define default values for nodes and edges
840
+ const nodes = rawNodes ?? [];
841
+ const edges = rawEdges ?? [];
842
+ // Parse nodes and edges using the defined functions
843
+ const parsedNodes = nodes.map((node) => parseNodeFromArrows(node));
844
+ const parsedEdges = edges.map((edge) => parseEdgeFromArrows(edge));
845
+ return {
846
+ nodes: parsedNodes,
847
+ edges: parsedEdges
848
+ };
849
+ };
850
+
851
+ const parseRichEdges = (body) => {
852
+ const parseSingleEdge = (edge) => {
853
+ const { label, fromKey, toKey, properties } = edge;
854
+ if (!label || typeof label !== "string")
855
+ throw new InvalidRequestError("Every graph edge must have string label");
856
+ if (!isInEnum(label, exports.UsedEdgeLabels))
857
+ throw new InvalidRequestError(`Graph edge label is not allowed (${label}). Must be one of: ${enumValuesToString(exports.UsedEdgeLabels)}`);
858
+ if (!fromKey || typeof fromKey !== "string")
859
+ throw new InvalidRequestError("Every graph edge must have string fromKey");
860
+ if (!toKey || typeof toKey !== "string")
861
+ throw new InvalidRequestError("Every graph edge must have string toKey");
862
+ if (fromKey === toKey)
863
+ throw new InvalidRequestError(`Cannot connect two same keys in graph edge (${fromKey})`);
864
+ const parsedEdge = {
865
+ label: label,
866
+ edgeNodes: [fromKey, toKey],
867
+ properties: properties || {}
868
+ };
869
+ return parsedEdge;
870
+ };
871
+ const edgesRaw = body;
872
+ if (!lodash.isArray(edgesRaw))
873
+ throw new InvalidRequestError("Graph edges must be an array of edges");
874
+ if (edgesRaw.length === 0)
875
+ throw new InvalidRequestError("Graph edges array must not be empty");
876
+ const parsedEdges = edgesRaw.map(edge => parseSingleEdge(edge));
877
+ return parsedEdges;
878
+ };
879
+ /**
880
+ * Parse body to graph relation
881
+ * @param body Body from request
882
+ * @returns Graph relation
883
+ */
884
+ const parseEqualEdges = (body) => {
885
+ const relations = body;
886
+ /**
887
+ * Check single edge relation and parse it
888
+ * @param edgeRelation
889
+ * @returns Parsed edge relation
890
+ */
891
+ const parseSingleEdgeRelation = (edgeRelation) => {
892
+ if (!lodash.isArray(edgeRelation))
893
+ throw new InvalidRequestError("Every graph relation must be two element string tuple [string, string]");
894
+ if (edgeRelation.length !== 2)
895
+ throw new InvalidRequestError("Every graph relation must be two element string tuple [string, string]");
896
+ if (edgeRelation[0] === edgeRelation[1])
897
+ throw new InvalidRequestError(`Cannot connect two same keys in graph relation (${edgeRelation[0]})`);
898
+ return edgeRelation;
899
+ };
900
+ if (!lodash.isArray(relations))
901
+ throw new InvalidRequestError("Graph edges must be an array of tuples");
902
+ const validatedGraphEdges = relations.map(edge => parseSingleEdgeRelation(edge));
903
+ return validatedGraphEdges;
904
+ };
905
+
196
906
  exports.AppLogger = AppLogger;
197
907
  exports.AuthorizationError = AuthorizationError;
198
908
  exports.InvalidRequestError = InvalidRequestError;
909
+ exports.csvParseNumbers = csvParseNumbers;
910
+ exports.csvParseStrings = csvParseStrings;
199
911
  exports.enumCombineValuesToString = enumCombineValuesToString;
200
912
  exports.enumValuesToArray = enumValuesToArray;
201
913
  exports.enumValuesToString = enumValuesToString;
914
+ exports.epochToIsoFormat = epochToIsoFormat;
915
+ exports.filterEdgeByLabel = filterEdgeByLabel;
916
+ exports.filterNodeByLabel = filterNodeByLabel;
917
+ exports.findEdgeByLabel = findEdgeByLabel;
918
+ exports.findNodeByLabel = findNodeByLabel;
202
919
  exports.flattenObject = flattenObject;
920
+ exports.hasIsoFormat = hasIsoFormat;
203
921
  exports.isInEnum = isInEnum;
922
+ exports.isoDateToTimestamp = isoDateToTimestamp;
923
+ exports.isoIntervalToTimestamps = isoIntervalToTimestamps;
204
924
  exports.messageFromError = messageFromError;
205
925
  exports.notEmptyString = notEmptyString;
926
+ exports.nowTimestamp = nowTimestamp;
927
+ exports.nowTimestampIso = nowTimestampIso;
928
+ exports.parseArrowsJson = parseArrowsJson;
929
+ exports.parseEqualEdges = parseEqualEdges;
930
+ exports.parseParsePantherNodes = parseParsePantherNodes;
931
+ exports.parseRichEdges = parseRichEdges;
932
+ exports.parseSinglePantherNode = parseSinglePantherNode;
206
933
  exports.randomNumberBetween = randomNumberBetween;
207
934
  exports.removeDuplicitiesFromArray = removeDuplicitiesFromArray;
208
935
  exports.sortStringArray = sortStringArray;
936
+ exports.validateEdgeLabel = validateEdgeLabel;
937
+ exports.validateNodeLabels = validateNodeLabels;
209
938
  //# sourceMappingURL=index.node.cjs.map