@uns-kit/api 2.0.45 → 2.0.46

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.
@@ -31,6 +31,21 @@ const buildSwaggerPath = (base, processName, instanceName) => {
31
31
  }
32
32
  return `${baseWithProcess}/${instanceName}/swagger.json`.replace(/\/{2,}/g, "/");
33
33
  };
34
+ const normalizeStringArray = (value) => Array.isArray(value)
35
+ ? Array.from(new Set(value
36
+ .map((entry) => (typeof entry === "string" ? entry.trim() : ""))
37
+ .filter((entry) => entry.length > 0)))
38
+ : [];
39
+ const normalizePathValue = (value) => {
40
+ if (typeof value !== "string") {
41
+ return null;
42
+ }
43
+ const trimmed = value.trim();
44
+ if (!trimmed) {
45
+ return null;
46
+ }
47
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
48
+ };
34
49
  export default class UnsApiProxy extends UnsProxy {
35
50
  instanceName;
36
51
  topicBuilder;
@@ -45,6 +60,7 @@ export default class UnsApiProxy extends UnsProxy {
45
60
  startedAt;
46
61
  statusInterval = null;
47
62
  statusIntervalMs = 10_000;
63
+ dataCatalogOffers = new Map();
48
64
  constructor(processName, instanceName, options) {
49
65
  super();
50
66
  this.options = options;
@@ -76,6 +92,9 @@ export default class UnsApiProxy extends UnsProxy {
76
92
  setTimeout(() => this.emitStatusMetrics(), 0);
77
93
  this.statusInterval = setInterval(() => this.emitStatusMetrics(), this.statusIntervalMs);
78
94
  }
95
+ getProcessName() {
96
+ return this.processName;
97
+ }
79
98
  /**
80
99
  * Unregister endpoint
81
100
  * @param topic - The API topic
@@ -138,11 +157,15 @@ export default class UnsApiProxy extends UnsProxy {
138
157
  timestamp: time,
139
158
  topic: topic,
140
159
  attribute: attribute,
160
+ routeOnly: options?.routeOnly === true,
161
+ registryTopic: options?.registryTopic ??
162
+ "api-endpoints",
141
163
  apiHost: `http://${ip}:${port}`,
142
164
  apiEndpoint: apiPath,
143
165
  apiMethod: "GET",
144
166
  apiQueryParams: options.queryParams,
145
167
  apiDescription: options?.apiDescription,
168
+ serviceApi: options?.serviceApi ?? null,
146
169
  attributeType: UnsAttributeType.Api,
147
170
  apiSwaggerEndpoint: swaggerPath,
148
171
  asset,
@@ -383,15 +406,19 @@ export default class UnsApiProxy extends UnsProxy {
383
406
  swaggerPath,
384
407
  });
385
408
  }
386
- /**
387
- * Register a POST endpoint.
388
- * @param topic - The API topic
389
- * @param attribute - The attribute for the topic.
390
- * @param options.apiDescription - Optional description.
391
- * @param options.tags - Optional tags.
392
- * @param options.requestBody - Optional request body schema for Swagger.
393
- */
394
409
  async post(topic, asset, objectType, objectId, attribute, options) {
410
+ await this.registerMutationEndpoint("POST", topic, asset, objectType, objectId, attribute, options);
411
+ }
412
+ async put(topic, asset, objectType, objectId, attribute, options) {
413
+ await this.registerMutationEndpoint("PUT", topic, asset, objectType, objectId, attribute, options);
414
+ }
415
+ async patch(topic, asset, objectType, objectId, attribute, options) {
416
+ await this.registerMutationEndpoint("PATCH", topic, asset, objectType, objectId, attribute, options);
417
+ }
418
+ async delete(topic, asset, objectType, objectId, attribute, options) {
419
+ await this.registerMutationEndpoint("DELETE", topic, asset, objectType, objectId, attribute, options);
420
+ }
421
+ async registerMutationEndpoint(method, topic, asset, objectType, objectId, attribute, options) {
395
422
  while (this.app.server.listening === false) {
396
423
  await new Promise((resolve) => setTimeout(resolve, 100));
397
424
  }
@@ -399,6 +426,7 @@ export default class UnsApiProxy extends UnsProxy {
399
426
  const fullPath = buildUnsRoutePath(topic, asset, objectType, objectId, attribute);
400
427
  const apiPath = `${this.apiBasePrefix}${fullPath}`.replace(/\/{2,}/g, "/");
401
428
  const swaggerPath = buildSwaggerPath(this.swaggerBasePrefix, this.processName, this.instanceName);
429
+ const methodKey = method.toLowerCase();
402
430
  try {
403
431
  const addressInfo = this.app.server.address();
404
432
  let ip;
@@ -415,11 +443,15 @@ export default class UnsApiProxy extends UnsProxy {
415
443
  timestamp: time,
416
444
  topic,
417
445
  attribute,
446
+ routeOnly: options?.routeOnly === true,
447
+ registryTopic: options?.registryTopic ??
448
+ "api-endpoints",
418
449
  apiHost: `http://${ip}:${port}`,
419
450
  apiEndpoint: apiPath,
420
- apiMethod: "POST",
451
+ apiMethod: method,
421
452
  apiQueryParams: [],
422
453
  apiDescription: options?.apiDescription,
454
+ serviceApi: options?.serviceApi ?? null,
423
455
  attributeType: UnsAttributeType.Api,
424
456
  apiSwaggerEndpoint: swaggerPath,
425
457
  asset,
@@ -427,10 +459,11 @@ export default class UnsApiProxy extends UnsProxy {
427
459
  objectId,
428
460
  });
429
461
  const handler = (req, res) => {
430
- this.event.emit("apiPostEvent", { req, res });
462
+ this.event.emit(this.getApiEventName(method), { req, res });
431
463
  };
464
+ const routerMethod = this.app.router[methodKey].bind(this.app.router);
432
465
  if (this.options?.jwks?.wellKnownJwksUrl) {
433
- this.app.router.post(fullPath, async (req, res) => {
466
+ routerMethod(fullPath, async (req, res) => {
434
467
  try {
435
468
  const token = this.extractBearerToken(req, res);
436
469
  if (!token)
@@ -451,13 +484,13 @@ export default class UnsApiProxy extends UnsProxy {
451
484
  }
452
485
  handler(req, res);
453
486
  }
454
- catch (err) {
487
+ catch (_err) {
455
488
  return res.status(401).json({ error: "Invalid token" });
456
489
  }
457
490
  });
458
491
  }
459
492
  else if (this.options?.jwtSecret) {
460
- this.app.router.post(fullPath, (req, res) => {
493
+ routerMethod(fullPath, (req, res) => {
461
494
  const authHeader = req.headers["authorization"];
462
495
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
463
496
  return res.status(401).json({ error: "Missing or invalid Authorization header" });
@@ -478,19 +511,19 @@ export default class UnsApiProxy extends UnsProxy {
478
511
  }
479
512
  handler(req, res);
480
513
  }
481
- catch (err) {
514
+ catch (_err) {
482
515
  return res.status(401).json({ error: "Invalid token" });
483
516
  }
484
517
  });
485
518
  }
486
519
  else {
487
- this.app.router.post(fullPath, handler);
520
+ routerMethod(fullPath, handler);
488
521
  }
489
522
  if (this.app.swaggerSpec) {
490
523
  const requestBody = options?.requestBody;
491
524
  this.app.swaggerSpec.paths = this.app.swaggerSpec.paths || {};
492
525
  this.app.swaggerSpec.paths[apiPath] = this.app.swaggerSpec.paths[apiPath] || {};
493
- this.app.swaggerSpec.paths[apiPath].post = {
526
+ this.app.swaggerSpec.paths[apiPath][methodKey] = {
494
527
  summary: options?.apiDescription || "No description",
495
528
  tags: options?.tags || [],
496
529
  ...(requestBody
@@ -516,9 +549,58 @@ export default class UnsApiProxy extends UnsProxy {
516
549
  }
517
550
  }
518
551
  catch (error) {
519
- logger.error(`${this.instanceNameWithSuffix} - Error registering POST route ${fullPath}: ${error.message}`);
552
+ logger.error(`${this.instanceNameWithSuffix} - Error registering ${method} route ${fullPath}: ${error.message}`);
520
553
  }
521
554
  }
555
+ registerDataOffer(input) {
556
+ const offerId = typeof input.offerId === "string" ? input.offerId.trim() : "";
557
+ const displayName = typeof input.displayName === "string" ? input.displayName.trim() : "";
558
+ if (!offerId || !displayName) {
559
+ throw new Error("Data catalog offer requires non-empty offerId and displayName.");
560
+ }
561
+ const offerSchemas = (Array.isArray(input.schemas) ? input.schemas : []).map((schema) => this.normalizeDataOfferSchema(schema));
562
+ const offerSchemaIndex = new Map(offerSchemas.map((schema) => [schema.id, schema]));
563
+ const normalizedOperations = (Array.isArray(input.operations) ? input.operations : [])
564
+ .map((operation, index) => this.normalizeDataOfferOperation(operation, offerId, index, offerSchemaIndex))
565
+ .filter((operation) => Boolean(operation));
566
+ if (!normalizedOperations.length) {
567
+ throw new Error(`Data catalog offer ${offerId} requires at least one operation.`);
568
+ }
569
+ const basePaths = Array.from(new Set([
570
+ ...(Array.isArray(input.basePaths) ? input.basePaths.map((entry) => normalizePathValue(entry)) : []),
571
+ ...normalizedOperations.map((operation) => {
572
+ const segments = operation.path.split("/").filter(Boolean);
573
+ if (segments.length <= 1) {
574
+ return operation.path;
575
+ }
576
+ return `/${segments.slice(0, -1).join("/")}`;
577
+ }),
578
+ ].filter((value) => Boolean(value))));
579
+ this.publishDataCatalogOffer({
580
+ offerId,
581
+ displayName,
582
+ description: typeof input.description === "string" ? input.description.trim() || null : null,
583
+ owner: typeof input.owner === "string" ? input.owner.trim() || null : null,
584
+ status: typeof input.status === "string" && input.status.trim() ? input.status.trim() : "available",
585
+ tags: normalizeStringArray(input.tags),
586
+ categories: normalizeStringArray(input.categories),
587
+ microserviceName: typeof input.microserviceName === "string" && input.microserviceName.trim()
588
+ ? input.microserviceName.trim()
589
+ : this.processName,
590
+ version: typeof input.version === "string" && input.version.trim() ? input.version.trim() : packageJson.version,
591
+ swaggerPath: normalizePathValue(input.swaggerPath),
592
+ basePaths,
593
+ operations: normalizedOperations,
594
+ schemas: offerSchemas,
595
+ metadata: input.metadata ?? null,
596
+ packageName: packageJson.name,
597
+ processName: this.processName,
598
+ processVersion: packageJson.version,
599
+ instanceName: this.instanceName,
600
+ controllerName: process.env["UNS_CONTROLLER_NAME"] ?? null,
601
+ controllerPublicBase: process.env["UNS_CONTROLLER_PUBLIC_BASE"] ?? process.env["UNS_PUBLIC_BASE"] ?? null,
602
+ });
603
+ }
522
604
  emitStatusMetrics() {
523
605
  const uptimeMinutes = Math.round((Date.now() - this.startedAt) / 60000);
524
606
  // Process-level status
@@ -547,6 +629,7 @@ export default class UnsApiProxy extends UnsProxy {
547
629
  uom: DataSizeMeasurements.Bit,
548
630
  statusTopic: this.instanceStatusTopic + "alive",
549
631
  });
632
+ this.emitDataCatalogOffers();
550
633
  }
551
634
  registerHealthEndpoint() {
552
635
  const routePath = "/status";
@@ -575,6 +658,20 @@ export default class UnsApiProxy extends UnsProxy {
575
658
  };
576
659
  }
577
660
  }
661
+ getApiEventName(method) {
662
+ switch (method) {
663
+ case "GET":
664
+ return "apiGetEvent";
665
+ case "POST":
666
+ return "apiPostEvent";
667
+ case "PUT":
668
+ return "apiPutEvent";
669
+ case "PATCH":
670
+ return "apiPatchEvent";
671
+ case "DELETE":
672
+ return "apiDeleteEvent";
673
+ }
674
+ }
578
675
  extractBearerToken(req, res) {
579
676
  const authHeader = req.headers["authorization"];
580
677
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
@@ -647,4 +744,117 @@ export default class UnsApiProxy extends UnsProxy {
647
744
  }
648
745
  await super.stop();
649
746
  }
747
+ publishDataCatalogOffer(offer) {
748
+ const offerId = typeof offer.offerId === "string" ? offer.offerId.trim() : "";
749
+ if (!offerId) {
750
+ return;
751
+ }
752
+ this.dataCatalogOffers.set(offerId, offer);
753
+ this.emitDataCatalogOffers();
754
+ logger.info(`${this.instanceNameWithSuffix} - Registered data catalog offer: ${offerId}`);
755
+ }
756
+ emitDataCatalogOffers() {
757
+ if (this.instanceStatusTopic === "" || this.dataCatalogOffers.size === 0) {
758
+ return;
759
+ }
760
+ this.event.emit("unsProxyProducedDataCatalogOffers", {
761
+ producedDataCatalogOffers: [...this.dataCatalogOffers.values()],
762
+ statusTopic: this.instanceStatusTopic + "data-catalog-offers",
763
+ });
764
+ }
765
+ normalizeDataOfferOperation(operation, offerId, index, offerSchemaIndex) {
766
+ const method = typeof operation.method === "string" ? operation.method.trim().toUpperCase() : "";
767
+ const path = normalizePathValue(operation.path);
768
+ if (!method || !path) {
769
+ return null;
770
+ }
771
+ return {
772
+ id: typeof operation.id === "string" && operation.id.trim() ? operation.id.trim() : `${offerId}-${method.toLowerCase()}-${index + 1}`,
773
+ method,
774
+ path,
775
+ summary: typeof operation.summary === "string" ? operation.summary.trim() || null : null,
776
+ description: typeof operation.description === "string" ? operation.description.trim() || null : null,
777
+ tags: normalizeStringArray(operation.tags),
778
+ deprecated: operation.deprecated === true,
779
+ parameters: (Array.isArray(operation.parameters) ? operation.parameters : []).map((parameter) => this.normalizeDataOfferParameter(parameter)),
780
+ headers: (Array.isArray(operation.headers) ? operation.headers : []).map((parameter) => this.normalizeDataOfferParameter(parameter)),
781
+ requestBody: operation.requestBody ? this.normalizeDataOfferRequestBody(operation.requestBody, offerSchemaIndex) : null,
782
+ responses: (Array.isArray(operation.responses) ? operation.responses : []).map((response) => this.normalizeDataOfferResponse(response, offerSchemaIndex)),
783
+ };
784
+ }
785
+ normalizeDataOfferParameter(parameter) {
786
+ return {
787
+ name: typeof parameter.name === "string" ? parameter.name.trim() : "",
788
+ in: typeof parameter.in === "string" ? parameter.in.trim() : "query",
789
+ required: parameter.required === true,
790
+ description: typeof parameter.description === "string" ? parameter.description.trim() || null : null,
791
+ type: typeof parameter.type === "string" && parameter.type.trim() ? parameter.type.trim() : "string",
792
+ format: typeof parameter.format === "string" ? parameter.format.trim() || null : null,
793
+ nullable: parameter.nullable === true,
794
+ example: parameter.example ?? null,
795
+ enumValues: normalizeStringArray(parameter.enumValues),
796
+ schema: parameter.schema ?? null,
797
+ };
798
+ }
799
+ normalizeDataOfferRequestBody(requestBody, offerSchemaIndex) {
800
+ return {
801
+ required: requestBody.required === true,
802
+ description: typeof requestBody.description === "string" ? requestBody.description.trim() || null : null,
803
+ contentType: typeof requestBody.contentType === "string" ? requestBody.contentType.trim() || null : null,
804
+ schemas: this.resolveOfferSchemas(requestBody.schemas, requestBody.schemaIds, offerSchemaIndex),
805
+ };
806
+ }
807
+ normalizeDataOfferResponse(response, offerSchemaIndex) {
808
+ return {
809
+ statusCode: typeof response.statusCode === "string" ? response.statusCode.trim() : "",
810
+ description: typeof response.description === "string" ? response.description.trim() || null : null,
811
+ contentType: typeof response.contentType === "string" ? response.contentType.trim() || null : null,
812
+ schemas: this.resolveOfferSchemas(response.schemas, response.schemaIds, offerSchemaIndex),
813
+ examplePayloads: Array.isArray(response.examplePayloads) ? response.examplePayloads : [],
814
+ headers: (Array.isArray(response.headers) ? response.headers : []).map((parameter) => this.normalizeDataOfferParameter(parameter)),
815
+ };
816
+ }
817
+ normalizeDataOfferSchema(schema) {
818
+ return {
819
+ id: typeof schema.id === "string" ? schema.id.trim() : "",
820
+ title: typeof schema.title === "string" ? schema.title.trim() : "",
821
+ kind: typeof schema.kind === "string" && schema.kind.trim() ? schema.kind.trim() : "schema",
822
+ source: typeof schema.source === "string" && schema.source.trim() ? schema.source.trim() : "registered",
823
+ contentType: typeof schema.contentType === "string" ? schema.contentType.trim() || null : null,
824
+ rootType: typeof schema.rootType === "string" && schema.rootType.trim() ? schema.rootType.trim() : "object",
825
+ nullable: schema.nullable === true,
826
+ description: typeof schema.description === "string" ? schema.description.trim() || null : null,
827
+ fields: (Array.isArray(schema.fields) ? schema.fields : []).map((field) => this.normalizeDataOfferSchemaField(field)),
828
+ examplePayloads: Array.isArray(schema.examplePayloads) ? schema.examplePayloads : [],
829
+ };
830
+ }
831
+ normalizeDataOfferSchemaField(field) {
832
+ const defaultName = typeof field.name === "string" ? field.name.trim() : "";
833
+ const path = typeof field.path === "string" && field.path.trim() ? field.path.trim() : defaultName;
834
+ return {
835
+ path,
836
+ name: defaultName || (path.split(".").at(-1) ?? path),
837
+ type: typeof field.type === "string" && field.type.trim() ? field.type.trim() : "string",
838
+ format: typeof field.format === "string" ? field.format.trim() || null : null,
839
+ nullable: field.nullable === true,
840
+ required: field.required === true,
841
+ description: typeof field.description === "string" ? field.description.trim() || null : null,
842
+ enumValues: normalizeStringArray(field.enumValues),
843
+ example: field.example ?? null,
844
+ };
845
+ }
846
+ resolveOfferSchemas(inlineSchemas, schemaIds, offerSchemaIndex) {
847
+ const result = (Array.isArray(inlineSchemas) ? inlineSchemas : []).map((schema) => this.normalizeDataOfferSchema(schema));
848
+ for (const schemaId of Array.isArray(schemaIds) ? schemaIds : []) {
849
+ const normalizedId = typeof schemaId === "string" ? schemaId.trim() : "";
850
+ if (!normalizedId) {
851
+ continue;
852
+ }
853
+ const resolved = offerSchemaIndex.get(normalizedId);
854
+ if (resolved && !result.some((schema) => schema.id === resolved.id)) {
855
+ result.push(resolved);
856
+ }
857
+ }
858
+ return result;
859
+ }
650
860
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uns-kit/api",
3
- "version": "2.0.45",
3
+ "version": "2.0.46",
4
4
  "description": "Express-powered API gateway plugin for UnsProxyProcess with JWT/JWKS support.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -31,16 +31,18 @@
31
31
  "main": "dist/index.js",
32
32
  "types": "dist/index.d.ts",
33
33
  "dependencies": {
34
- "jsonwebtoken": "^9.0.2",
35
34
  "cookie-parser": "^1.4.7",
36
35
  "express": "^5.1.0",
36
+ "jsonwebtoken": "^9.0.2",
37
37
  "multer": "^2.0.2",
38
- "@uns-kit/core": "2.0.45"
38
+ "parquets": "^0.10.10",
39
+ "uuid": "^14.0.0",
40
+ "@uns-kit/core": "2.0.46"
39
41
  },
40
42
  "devDependencies": {
41
- "@types/jsonwebtoken": "^9.0.10",
42
43
  "@types/cookie-parser": "^1.4.9",
43
44
  "@types/express": "^5.0.3",
45
+ "@types/jsonwebtoken": "^9.0.10",
44
46
  "@types/multer": "^2.0.0"
45
47
  },
46
48
  "scripts": {