diff --git a/client/src/app/_models/device.ts b/client/src/app/_models/device.ts index 90c2a3d1d..e726f2a37 100644 --- a/client/src/app/_models/device.ts +++ b/client/src/app/_models/device.ts @@ -96,6 +96,8 @@ export class Tag { sysType: TagSystemType; /** Description */ description?: string; + /** Optional Unified Namespace path */ + unsPath?: string | null; /** Deadband to set changed value */ deadband?: TagDeadband; /** diff --git a/client/src/app/_models/settings.ts b/client/src/app/_models/settings.ts index b5364f3c1..49b098301 100644 --- a/client/src/app/_models/settings.ts +++ b/client/src/app/_models/settings.ts @@ -64,6 +64,9 @@ export class DaqStore { type = DaqStoreType.SQlite; varsion?: string; url?: string; + host = '127.0.0.1'; + tableName = 'meters'; + configurationString = 'http::addr=localhost:9000;'; organization?: string; credentials?: StoreCredentials; bucket?: string; @@ -74,6 +77,9 @@ export class DaqStore { if (daqstore) { this.type = daqstore.type; this.url = daqstore.url; + this.host = daqstore.host; + this.tableName = daqstore.tableName || 'meters'; + this.configurationString = daqstore.configurationString; this.organization = daqstore.organization; this.credentials = daqstore.credentials; this.bucket = daqstore.bucket; @@ -84,6 +90,7 @@ export class DaqStore { isEquals(store: DaqStore) { if (this.type === store.type && this.bucket === store.bucket && this.url === store.url && + this.host === store.host && this.tableName === store.tableName && this.configurationString === store.configurationString && this.organization === store.organization && this.database === store.database && (this.credentials && StoreCredentials.isEquals(this.credentials, store.credentials)) && this.retention === store.retention) { return true; @@ -115,6 +122,7 @@ export enum DaqStoreType { influxDB = 'influxDB', influxDB18 = 'influxDB 1.8', TDengine = 'TDengine' , + questDB = 'questDB', } export enum influxDBVersionType { diff --git a/client/src/app/device/device-list/device-list.component.html b/client/src/app/device/device-list/device-list.component.html index 8c756a931..45210137e 100644 --- a/client/src/app/device/device-list/device-list.component.html +++ b/client/src/app/device/device-list/device-list.component.html @@ -86,6 +86,14 @@ {{ element.description }} + + + + + account_tree + + + @@ -159,4 +167,4 @@ - \ No newline at end of file + diff --git a/client/src/app/device/device-list/device-list.component.ts b/client/src/app/device/device-list/device-list.component.ts index 67a64f781..6a9eb866f 100644 --- a/client/src/app/device/device-list/device-list.component.ts +++ b/client/src/app/device/device-list/device-list.component.ts @@ -25,11 +25,11 @@ import { TagPropertyService } from '../tag-property/tag-property.service'; }) export class DeviceListComponent implements OnInit, AfterViewInit { - readonly defAllColumns = ['select', 'name', 'address', 'device', 'type', 'value', 'timestamp', 'description', 'warning', 'logger', 'options', 'remove']; - readonly defAllExtColumns = ['select', 'name', 'address', 'device', 'type', 'value', 'timestamp', 'quality', 'description', 'warning', 'logger', 'options', 'remove']; + readonly defAllColumns = ['select', 'name', 'address', 'device', 'type', 'value', 'timestamp', 'description', 'warning', 'uns', 'logger', 'options', 'remove']; + readonly defAllExtColumns = ['select', 'name', 'address', 'device', 'type', 'value', 'timestamp', 'quality', 'description', 'warning', 'uns', 'logger', 'options', 'remove']; readonly defInternalColumns = ['select', 'name', 'device', 'type', 'value', 'timestamp', 'description', 'options', 'remove']; - readonly defGpipColumns = ['select', 'name', 'device', 'address', 'direction', 'value', 'timestamp', 'description', 'logger', 'options', 'remove']; - readonly defWebcamColumns = ['select', 'name', 'device', 'address', 'value', 'timestamp', 'description', 'logger', 'options', 'remove']; + readonly defGpipColumns = ['select', 'name', 'device', 'address', 'direction', 'value', 'timestamp', 'description', 'uns', 'logger', 'options', 'remove']; + readonly defWebcamColumns = ['select', 'name', 'device', 'address', 'value', 'timestamp', 'description', 'uns', 'logger', 'options', 'remove']; readonly defAllRowWidth = 1400; readonly defClientRowWidth = 1400; readonly defInternalRowWidth = 1200; @@ -383,6 +383,7 @@ export class DeviceListComponent implements OnInit, AfterViewInit { tags[i].scaleReadParams = tagOption.scaleReadParams; tags[i].scaleWriteFunction = tagOption.scaleWriteFunction; tags[i].scaleWriteParams = tagOption.scaleWriteParams; + tags[i].unsPath = tagOption.unsPath; } this.projectService.setDeviceTags(this.deviceSelected); } diff --git a/client/src/app/device/tag-options/tag-options.component.html b/client/src/app/device/tag-options/tag-options.component.html index 6d032d95c..0976e6d66 100644 --- a/client/src/app/device/tag-options/tag-options.component.html +++ b/client/src/app/device/tag-options/tag-options.component.html @@ -29,6 +29,13 @@

{{'device.tag-deadband' | translate}} +
+ {{'device.tag-uns-path' | translate}} + + + {{'msg.device-tag-exist' | translate}} + +
{{'device.tag-scale' | translate}} diff --git a/client/src/app/device/tag-options/tag-options.component.ts b/client/src/app/device/tag-options/tag-options.component.ts index be1ebff50..c6f0f3f16 100644 --- a/client/src/app/device/tag-options/tag-options.component.ts +++ b/client/src/app/device/tag-options/tag-options.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, Inject, OnDestroy, OnInit } from '@angular/core'; -import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, ValidationErrors, ValidatorFn, Validators } from '@angular/forms'; import { MatDialogRef as MatDialogRef, MAT_DIALOG_DATA as MAT_DIALOG_DATA } from '@angular/material/dialog'; import { FuxaServer, TagDaq, TagDeadband, TagDeadbandModeType, TagScale, TagScaleModeType } from '../../_models/device'; import { Utils} from '../../_helpers/utils'; @@ -56,6 +56,7 @@ export class TagOptionsComponent implements OnInit, OnDestroy { scaleWriteFunction: null, scaleReadExpression: null, scaleWriteExpression: null, + unsPath: [null, [this.validateUnsPathUnique()]], }); this.formGroup.controls.enabled.valueChanges.subscribe(enabled => { @@ -88,6 +89,7 @@ export class TagOptionsComponent implements OnInit, OnDestroy { //let scaleWriteParams = { value: null, valid: true }; let scaleReadExpression = { value: null, valid: true }; let scaleWriteExpression = { value: null, valid: true }; + let unsPath = { value: null, valid: true, initialized: false }; for (let i = 0; i < this.data.tags.length; i++) { if (!this.data.tags[i].daq) { continue; @@ -153,6 +155,14 @@ export class TagOptionsComponent implements OnInit, OnDestroy { const tagParams = JSON.parse(this.data.tags[i].scaleWriteParams) as ScriptParam[]; const notValid = this.initializeScriptParams(script, tagParams, this.configedWriteParams); } + + const currentUnsPath = this.normalizeUnsPath(this.data.tags[i].unsPath); + if (!unsPath.initialized) { + unsPath.value = currentUnsPath; + unsPath.initialized = true; + } else if (unsPath.value !== currentUnsPath) { + unsPath.valid = false; + } } let values = {}; if (enabled.valid && enabled.value !== null) { @@ -192,6 +202,9 @@ export class TagOptionsComponent implements OnInit, OnDestroy { if (scaleWriteFunction.valid && scaleWriteFunction.value) { values = {...values, scaleWriteFunction: scaleWriteFunction.value}; } + if (unsPath.valid && unsPath.initialized) { + values = {...values, unsPath: unsPath.value}; + } this.formGroup.patchValue(values); if (this.data.device?.id === FuxaServer.id) { @@ -274,7 +287,8 @@ export class TagOptionsComponent implements OnInit, OnDestroy { scaleReadFunction: this.formGroup.value.scaleReadFunction, scaleReadParams: readParamsStr, scaleWriteFunction: this.formGroup.value.scaleWriteFunction, - scaleWriteParams: writeParamsStr + scaleWriteParams: writeParamsStr, + unsPath: this.normalizeUnsPath(this.formGroup.value.unsPath) }); } @@ -368,6 +382,30 @@ export class TagOptionsComponent implements OnInit, OnDestroy { } return true; } + + private validateUnsPathUnique(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const currentUnsPath = this.normalizeUnsPath(control?.value); + if (!currentUnsPath || !this.data?.device?.tags) { + return null; + } + const currentTagIds = new Set((this.data?.tags || []).map(t => t.id)); + const exists = Object.values(this.data.device.tags).some((tag: any) => + tag && + !currentTagIds.has(tag.id) && + this.normalizeUnsPath(tag.unsPath) === currentUnsPath + ); + return exists ? { unsPathExists: true } : null; + }; + } + + private normalizeUnsPath(value: any): string | null { + if (Utils.isNullOrUndefined(value)) { + return null; + } + const normalized = String(value).trim(); + return normalized.length ? normalized : null; + } } export interface TagOptionType { @@ -379,4 +417,5 @@ export interface TagOptionType { scaleReadParams?: string; scaleWriteFunction?: string; scaleWriteParams?: string; + unsPath?: string | null; } diff --git a/client/src/app/editor/app-settings/app-settings.component.html b/client/src/app/editor/app-settings/app-settings.component.html index 82e5231cf..951b16834 100644 --- a/client/src/app/editor/app-settings/app-settings.component.html +++ b/client/src/app/editor/app-settings/app-settings.component.html @@ -203,6 +203,34 @@

+
+
+ {{'dlg.app-settings-daqstore-queryconn' | translate}} +
+
+ {{'dlg.app-settings-daqstore-questdb-host' | translate}} + +
+
+ {{'dlg.app-settings-daqstore-database' | translate}} + +
+
+ {{'dlg.app-settings-daqstore-username' | translate}} + +
+
+ {{'dlg.app-settings-daqstore-password' | translate}} + +
+
+ {{'dlg.app-settings-daqstore-ingestconn' | translate}} +
+
+ {{'dlg.app-settings-daqstore-configurl' | translate}} + +
+
diff --git a/client/src/assets/i18n/de.json b/client/src/assets/i18n/de.json index f730dc8b8..3f5ec9e8e 100644 --- a/client/src/assets/i18n/de.json +++ b/client/src/assets/i18n/de.json @@ -834,6 +834,8 @@ "device.tag-daq-changed": "Wert speichern, wenn geändert", "device.tag-daq-interval": "Intervall für Speichern (Sek.)", "device.tag-format": "Ziffern formatieren (2 = #.##)", + + "device.tag-uns-path": "UNS Path", "device.tag-scale": "Skalierungsmodus", "device.tag-scale-mode-undefined": "Keine Skalierung", "device.tag-scale-mode-undefined-tooltip": "Tag-Wert", @@ -1240,6 +1242,11 @@ "dlg.app-settings-daqstore": "DAQ-Speicher", "dlg.app-settings-daqstore-type": "Datenbanktyp", "dlg.app-settings-daqstore-url": "URL", + "dlg.app-settings-daqstore-configurl": "Config URL", + "dlg.app-settings-daqstore-queryconn": "Query connection (PGWire)", + + "dlg.app-settings-daqstore-questdb-host": "QuestDB Host", + "dlg.app-settings-daqstore-ingestconn": "Ingestion connection (ILP)", "dlg.app-settings-daqstore-token": "Token", "dlg.app-settings-daqstore-bucket": "Bucket", "dlg.app-settings-daqstore-organization": "Organisation", @@ -1306,3 +1313,6 @@ "msg.report-build-error": "Senden zum Erstellen des Berichts fehlgeschlagen!", "msg.device-tags-request-result": "Lade {{value}} von {{current}}" } + + + diff --git a/client/src/assets/i18n/en.json b/client/src/assets/i18n/en.json index d6bd98cc9..7699e70f1 100644 --- a/client/src/assets/i18n/en.json +++ b/client/src/assets/i18n/en.json @@ -1154,6 +1154,8 @@ "device.tag-format": "Format digits (2 = #.##)", "device.tag-deadband": "Deadband", + + "device.tag-uns-path": "UNS Path", "device.tag-scale": "Scale Mode / Convertion", "device.tag-scale-mode-undefined": "No Scaling", "device.tag-scale-mode-undefined-tooltip": "Tag Value", @@ -1812,6 +1814,11 @@ "dlg.app-settings-daqstore": "DAQ storage", "dlg.app-settings-daqstore-type": "Database type", "dlg.app-settings-daqstore-url": "URL", + "dlg.app-settings-daqstore-configurl": "Config URL", + "dlg.app-settings-daqstore-queryconn": "Query connection (PGWire)", + + "dlg.app-settings-daqstore-questdb-host": "QuestDB Host", + "dlg.app-settings-daqstore-ingestconn": "Ingestion connection (ILP)", "dlg.app-settings-daqstore-token": "Token", "dlg.app-settings-daqstore-bucket": "Bucket", "dlg.app-settings-daqstore-organization": "Organization", @@ -1931,3 +1938,6 @@ "msg.operation-unauthorized": "Operation Unauthorized!", "msg.secret-code-required": "Secret code required to sign authentication tokens" } + + + diff --git a/client/src/assets/i18n/es.json b/client/src/assets/i18n/es.json index 9de460df5..54c2741da 100644 --- a/client/src/assets/i18n/es.json +++ b/client/src/assets/i18n/es.json @@ -544,6 +544,9 @@ "device-tag-dialog-title": "Etiqueta seleccionada", "device.tag-options-title": "Opciones de etiquetas", + + + "device.tag-uns-path": "UNS Path", "device.tag-daq-enabled": "Registration Enabled", "device.tag-daq-changed": "Guardar valor si cambia", "device.tag-daq-interval": "Intervalo para guardar valor (segundos)", @@ -736,6 +739,12 @@ "dlg.app-settings-server-port": "Servidor esta escuchando el puerto", "dlg.app-settings-alarms-clear": "Borrar todas las alarmas y el historias", "dlg.app-settings-auth-token": "Autenticación con token", + "dlg.app-settings-daqstore-url": "URL", + "dlg.app-settings-daqstore-configurl": "Config URL", + "dlg.app-settings-daqstore-queryconn": "Query connection (PGWire)", + + "dlg.app-settings-daqstore-questdb-host": "QuestDB Host", + "dlg.app-settings-daqstore-ingestconn": "Ingestion connection (ILP)", "dlg.app-auth-disabled": "Deshabilitado", "dlg.app-auth-expiration-15m": "Habilitado con token expira en 15 min.", "dlg.app-auth-expiration-1h": "Habilitado con token expira en 1 hora.", @@ -774,3 +783,6 @@ "msg.editor-mode-locked": "El editor ya esta abierto!", "msg.alarms-clear-success": "Todas las alarmas han sido canceladas!" } + + + diff --git a/client/src/assets/i18n/fr.json b/client/src/assets/i18n/fr.json index c51af46be..746ddaa27 100755 --- a/client/src/assets/i18n/fr.json +++ b/client/src/assets/i18n/fr.json @@ -1009,6 +1009,8 @@ "device.tag-format": "Format du nombre (2 = #.##)", "device.tag-deadband": "Zone morte", + + "device.tag-uns-path": "UNS Path", "device.tag-scale": "Mode mise à l'échelle / Conversion", "device.tag-scale-mode-undefined": "Pas de mise à l'échelle", "device.tag-scale-mode-undefined-tooltip": "Valeur du tag", @@ -1618,6 +1620,11 @@ "dlg.app-settings-daqstore": "Stockage DAQ", "dlg.app-settings-daqstore-type": "Type de base de données", "dlg.app-settings-daqstore-url": "URL", + "dlg.app-settings-daqstore-configurl": "Config URL", + "dlg.app-settings-daqstore-queryconn": "Query connection (PGWire)", + + "dlg.app-settings-daqstore-questdb-host": "QuestDB Host", + "dlg.app-settings-daqstore-ingestconn": "Ingestion connection (ILP)", "dlg.app-settings-daqstore-token": "Jeton", "dlg.app-settings-daqstore-bucket": "Seau", "dlg.app-settings-daqstore-organization": "Organisation", @@ -1715,3 +1722,6 @@ "msg.texts-text-remove": "Souhaitez-vous supprimer le texte '{{value}}' ?", "msg.operation-unauthorized": "Opération non autorisée !" } + + + diff --git a/client/src/assets/i18n/ja.json b/client/src/assets/i18n/ja.json index dfb6be4a4..df55c4d84 100644 --- a/client/src/assets/i18n/ja.json +++ b/client/src/assets/i18n/ja.json @@ -1031,6 +1031,8 @@ "device.tag-format": "桁数フォーマット (2 = #.##)", "device.tag-deadband": "デッドバンド", + + "device.tag-uns-path": "UNS Path", "device.tag-scale": "スケーリングモード / 変換", "device. tag-scale-mode-undefined": "スケーリングなし", "device.tag-scale-mode-undefined-tooltip": "タグ値", @@ -1652,6 +1654,11 @@ "dlg.app-settings-daqstore": "DAQストレージ", "dlg.app-settings-daqstore-type": "データベースタイプ", "dlg.app-settings-daqstore-url": "URL", + "dlg.app-settings-daqstore-configurl": "Config URL", + "dlg.app-settings-daqstore-queryconn": "Query connection (PGWire)", + + "dlg.app-settings-daqstore-questdb-host": "QuestDB Host", + "dlg.app-settings-daqstore-ingestconn": "Ingestion connection (ILP)", "dlg.app-settings-daqstore-token": "トークン", "dlg.app-settings-daqstore-bucket": "バケット", "dlg.app-settings-daqstore-organization": "組織", @@ -1748,4 +1755,6 @@ "msg.text-name-exist": "テキスト名が既に存在します!", "msg.texts-text-remove": "テキスト '{{value}}' を削除しますか?", "msg.operation-unauthorized": "操作が許可されていません!" -} \ No newline at end of file +} + + diff --git a/client/src/assets/i18n/ko.json b/client/src/assets/i18n/ko.json index 47dfa4d04..5305bb971 100644 --- a/client/src/assets/i18n/ko.json +++ b/client/src/assets/i18n/ko.json @@ -544,6 +544,9 @@ "device-tag-dialog-title": "태그 선택", "device.tag-options-title": "태그 옵션", + + + "device.tag-uns-path": "UNS Path", "device.tag-daq-enabled": "등록 활성화", "device.tag-daq-changed": "값 변경 가능시 저장", "device.tag-daq-interval": "값 차이 저장 (sec.)", @@ -734,6 +737,12 @@ "dlg.app-settings-server-port": "서버가 포팅 수신 대기중", "dlg.app-settings-alarms-clear": "모든 알림과 기록 삭제", "dlg.app-settings-auth-token": "토큰 권한", + "dlg.app-settings-daqstore-url": "URL", + "dlg.app-settings-daqstore-configurl": "Config URL", + "dlg.app-settings-daqstore-queryconn": "Query connection (PGWire)", + + "dlg.app-settings-daqstore-questdb-host": "QuestDB Host", + "dlg.app-settings-daqstore-ingestconn": "Ingestion connection (ILP)", "dlg.app-auth-disabled": "비활성화", "dlg.app-auth-expiration-15m": "15분 후 토큰 만료로 활성화.", "dlg.app-auth-expiration-1h": "1시간 후 토큰 만료로 활성화.", @@ -772,3 +781,6 @@ "msg.editor-mode-locked": "편집자가 이미 열려있습니다!", "msg.alarms-clear-success": "모든 알림이 취소되었습니다!" } + + + diff --git a/client/src/assets/i18n/pt.json b/client/src/assets/i18n/pt.json index 298abcad7..be4e4b396 100644 --- a/client/src/assets/i18n/pt.json +++ b/client/src/assets/i18n/pt.json @@ -625,6 +625,12 @@ "dlg.app-language-zh-cn": "Chinês", "dlg.app-settings-server-port": "Servidor está à escuta no porto", "dlg.app-settings-auth-token": "Autentificar com Token", + "dlg.app-settings-daqstore-url": "URL", + "dlg.app-settings-daqstore-configurl": "Config URL", + "dlg.app-settings-daqstore-queryconn": "Query connection (PGWire)", + + "dlg.app-settings-daqstore-questdb-host": "QuestDB Host", + "dlg.app-settings-daqstore-ingestconn": "Ingestion connection (ILP)", "dlg.app-auth-disabled": "Desativado", "dlg.app-auth-expiration-15m": "Ativar token com expiração em 15 minutos.", "dlg.app-auth-expiration-1h": "Ativar token com expiração em 1 horas.", @@ -656,5 +662,6 @@ "msg.project-save-ask": "De certeza que quer sair do Projeto?", "msg.login-username-required": "Entra o Utilizador", "msg.login-password-required": "Entra uma palavra-passe", - "msg.signin-failed": "Não autorizado" -} + "msg.signin-failed": "Não autorizado", + "device.tag-uns-path": "UNS Path" +} \ No newline at end of file diff --git a/client/src/assets/i18n/ru.json b/client/src/assets/i18n/ru.json index 0676f8952..75997a688 100644 --- a/client/src/assets/i18n/ru.json +++ b/client/src/assets/i18n/ru.json @@ -782,6 +782,9 @@ "device-tag-dialog-title": "Выбор тега", "device.tag-options-title": "Свойства тегов", + + + "device.tag-uns-path": "UNS Path", "device.tag-daq-enabled": "Регистрация включена", "device.tag-daq-changed": "Сохранять значения по изменению", "device.tag-daq-interval": "Интервал сохранения значений (сек.)", @@ -1170,6 +1173,11 @@ "dlg.app-settings-daqstore": "Хранилище данных", "dlg.app-settings-daqstore-type": "СУБД", "dlg.app-settings-daqstore-url": "URL", + "dlg.app-settings-daqstore-configurl": "Config URL", + "dlg.app-settings-daqstore-queryconn": "Query connection (PGWire)", + + "dlg.app-settings-daqstore-questdb-host": "QuestDB Host", + "dlg.app-settings-daqstore-ingestconn": "Ingestion connection (ILP)", "dlg.app-settings-daqstore-token": "Token", "dlg.app-settings-daqstore-bucket": "Bucket", "dlg.app-settings-daqstore-organization": "Организация", @@ -1226,3 +1234,6 @@ "msg.report-build-error": "Send to build Report failed!", "msg.device-tags-request-result": "Загружено {{value}} из {{current}}" } + + + diff --git a/client/src/assets/i18n/sv.json b/client/src/assets/i18n/sv.json index 8d7b01e4c..6afafc837 100644 --- a/client/src/assets/i18n/sv.json +++ b/client/src/assets/i18n/sv.json @@ -1003,6 +1003,8 @@ "device.tag-format": "Format decimaler (2 = #.##)", "device.tag-deadband": "Deadband", + + "device.tag-uns-path": "UNS Path", "device.tag-scale": "Skalningsläge / Konvertering", "device.tag-scale-mode-undefined": "Ingen skalning", "device.tag-scale-mode-undefined-tooltip": "Taggvärde", @@ -1599,6 +1601,11 @@ "dlg.app-settings-daqstore": "DAQ-lagring", "dlg.app-settings-daqstore-type": "Databastyp", "dlg.app-settings-daqstore-url": "URL", + "dlg.app-settings-daqstore-configurl": "Config URL", + "dlg.app-settings-daqstore-queryconn": "Query connection (PGWire)", + + "dlg.app-settings-daqstore-questdb-host": "QuestDB Host", + "dlg.app-settings-daqstore-ingestconn": "Ingestion connection (ILP)", "dlg.app-settings-daqstore-token": "Token", "dlg.app-settings-daqstore-bucket": "Bucket", "dlg.app-settings-daqstore-organization": "Organisation", @@ -1693,4 +1700,6 @@ "msg.text-name-exist": "Textnamnet finns redan!", "msg.texts-text-remove": "Vill du ta bort texten '{{value}}'?", "msg.operation-unauthorized": "Operation obehörig!" -} \ No newline at end of file +} + + diff --git a/client/src/assets/i18n/tr.json b/client/src/assets/i18n/tr.json index 0643a97d5..6a2888651 100644 --- a/client/src/assets/i18n/tr.json +++ b/client/src/assets/i18n/tr.json @@ -638,6 +638,12 @@ "dlg.app-language-tr": "Turkish", "dlg.app-settings-server-port": "Sunucuya erişmek için port", "dlg.app-settings-auth-token": "Anahtar şifre işe yetkilendir", + "dlg.app-settings-daqstore-url": "URL", + "dlg.app-settings-daqstore-configurl": "Config URL", + "dlg.app-settings-daqstore-queryconn": "Query connection (PGWire)", + + "dlg.app-settings-daqstore-questdb-host": "QuestDB Host", + "dlg.app-settings-daqstore-ingestconn": "Ingestion connection (ILP)", "dlg.app-auth-disabled": "Etkisiz", "dlg.app-auth-expiration-15m": "Anahtar şifre 15dk içinde geçerliliğini yitirecek.", @@ -669,5 +675,6 @@ "msg.project-save-ask": "Projeden çıkmak istiyor musunuz?", "msg.login-username-required": "Kullanıcı adı girin", "msg.login-password-required": "Şifre girin", - "msg.signin-failed": "Yetkisiz giriş." + "msg.signin-failed": "Yetkisiz giriş.", + "device.tag-uns-path": "UNS Path" } \ No newline at end of file diff --git a/client/src/assets/i18n/ua.json b/client/src/assets/i18n/ua.json index d93e644fa..ad0451fbd 100644 --- a/client/src/assets/i18n/ua.json +++ b/client/src/assets/i18n/ua.json @@ -578,6 +578,12 @@ "dlg.app-language-ua": "Ukrainian", "dlg.app-settings-server-port": "Порт сервера", "dlg.app-settings-auth-token": "Авторизація з токеном", + "dlg.app-settings-daqstore-url": "URL", + "dlg.app-settings-daqstore-configurl": "Config URL", + "dlg.app-settings-daqstore-queryconn": "Query connection (PGWire)", + + "dlg.app-settings-daqstore-questdb-host": "QuestDB Host", + "dlg.app-settings-daqstore-ingestconn": "Ingestion connection (ILP)", "dlg.app-auth-disabled": "Вимкнено", "dlg.app-auth-expiration-15m": "Увімкнено. Автоматичний вихід через 15хв.", "dlg.app-auth-expiration-1h": "Увімкнено. Автоматичний вихід через 1год.", @@ -610,5 +616,6 @@ "msg.project-save-ask": "При закритті вікна всі не збережені зміни пропадуть!", "msg.login-username-required": "Користувач", "msg.login-password-required": "Пароль", - "msg.signin-failed": "Не авторизований" -} + "msg.signin-failed": "Не авторизований", + "device.tag-uns-path": "UNS Path" +} \ No newline at end of file diff --git a/client/src/assets/i18n/zh-cn.json b/client/src/assets/i18n/zh-cn.json index 04b0e768e..b57a97eec 100644 --- a/client/src/assets/i18n/zh-cn.json +++ b/client/src/assets/i18n/zh-cn.json @@ -1010,6 +1010,8 @@ "device.tag-format": "格式化数字(2 = #.##)", "device.tag-deadband": "死区", + + "device.tag-uns-path": "UNS Path", "device.tag-scale": "刻度模式", "device.tag-scale-mode-undefined": "无缩放", "device.tag-scale-mode-undefined-tooltip": "标签值", @@ -1621,6 +1623,11 @@ "dlg.app-settings-daqstore": "DAQ存储", "dlg.app-settings-daqstore-type": "数据库类型", "dlg.app-settings-daqstore-url": "URL", + "dlg.app-settings-daqstore-configurl": "Config URL", + "dlg.app-settings-daqstore-queryconn": "Query connection (PGWire)", + + "dlg.app-settings-daqstore-questdb-host": "QuestDB Host", + "dlg.app-settings-daqstore-ingestconn": "Ingestion connection (ILP)", "dlg.app-settings-daqstore-token":"令牌", "dlg.app-settings-daqstore-bucket": "Bucket", "dlg.app-settings-daqstore-organization": "组织", @@ -1718,3 +1725,6 @@ "msg.texts-text-remove": "您是否想删除文本'{{value}}'?", "msg.operation-unauthorized": "操作未经授权!" } + + + diff --git a/client/src/assets/i18n/zh-tw.json b/client/src/assets/i18n/zh-tw.json index 7a288cfb0..ec201dfe1 100644 --- a/client/src/assets/i18n/zh-tw.json +++ b/client/src/assets/i18n/zh-tw.json @@ -1,4 +1,4 @@ -{ +{ "with param": "示例 {{value}}", "app.home": "首頁", @@ -1009,6 +1009,8 @@ "device.tag-format": "數字格式化(2 = #.##)", "device.tag-deadband": "死區", + + "device.tag-uns-path": "UNS Path", "device.tag-scale": "縮放模式", "device.tag-scale-mode-undefined": "無縮放", "device.tag-scale-mode-undefined-tooltip": "標籤值", @@ -1619,6 +1621,11 @@ "dlg.app-settings-daqstore": "DAQ 儲存", "dlg.app-settings-daqstore-type": "資料庫類型", "dlg.app-settings-daqstore-url": "URL", + "dlg.app-settings-daqstore-configurl": "Config URL", + "dlg.app-settings-daqstore-queryconn": "Query connection (PGWire)", + + "dlg.app-settings-daqstore-questdb-host": "QuestDB Host", + "dlg.app-settings-daqstore-ingestconn": "Ingestion connection (ILP)", "dlg.app-settings-daqstore-token":"權杖", "dlg.app-settings-daqstore-bucket": "Bucket", "dlg.app-settings-daqstore-organization": "組織", @@ -1716,3 +1723,6 @@ "msg.texts-text-remove": "您是否要刪除文字 '{{value}}'?", "msg.operation-unauthorized": "操作未授權!" } + + + diff --git a/server/package-lock.json b/server/package-lock.json index 010790393..d97ccea81 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,15 +1,16 @@ { "name": "fuxa-server", - "version": "1.3.0-2700", + "version": "1.3.0-2727", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fuxa-server", - "version": "1.3.0-2700", + "version": "1.3.0-2727", "license": "MIT", "dependencies": { "@influxdata/influxdb-client": "1.25.0", + "@questdb/nodejs-client": "^3.0.0", "@tdengine/rest": "3.0.2", "ads-client": "2.1.0", "app-root-path": "2.2.1", @@ -40,6 +41,7 @@ "nopt": "5.0.0", "odbc": "2.4.9", "pdfmake": "0.2.5", + "pg": "^8.18.0", "socket.io": "4.8.1", "sqlite3": "5.1.5", "swagger-ui-express": "^5.0.1", @@ -1197,6 +1199,12 @@ "tsyringe": "^4.8.0" } }, + "node_modules/@questdb/nodejs-client": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@questdb/nodejs-client/-/nodejs-client-3.0.0.tgz", + "integrity": "sha512-lBKd732rRpS/pqyWgCJFVXi9N1YoWDAUZzp6BngBVxH92As/NDXigZmCYKQVKpAEGYx14606CH5AoJ23qf3oqA==", + "license": "Apache-2.0" + }, "node_modules/@scarf/scarf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", @@ -3531,6 +3539,7 @@ "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -7541,6 +7550,105 @@ "resolved": "https://registry.npmmirror.com/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, + "node_modules/pg": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", + "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "pg-connection-string": "^2.11.0", + "pg-pool": "^3.11.0", + "pg-protocol": "^1.11.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz", + "integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz", + "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", + "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/pgpass/node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://mirrors.cloud.tencent.com/npm/picomatch/-/picomatch-2.3.1.tgz", @@ -7566,6 +7674,45 @@ "node": ">= 0.4" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/precond": { "version": "0.2.3", "resolved": "https://registry.npmmirror.com/precond/-/precond-0.2.3.tgz", @@ -8839,6 +8986,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "devOptional": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/server/package.json b/server/package.json index 29867e3bf..712db4caa 100644 --- a/server/package.json +++ b/server/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@influxdata/influxdb-client": "1.25.0", + "@questdb/nodejs-client": "^3.0.0", "@tdengine/rest": "3.0.2", "ads-client": "2.1.0", "app-root-path": "2.2.1", @@ -46,6 +47,7 @@ "nopt": "5.0.0", "odbc": "2.4.9", "pdfmake": "0.2.5", + "pg": "^8.18.0", "socket.io": "4.8.1", "sqlite3": "5.1.5", "swagger-ui-express": "^5.0.1", diff --git a/server/runtime/storage/daqstorage.js b/server/runtime/storage/daqstorage.js index ea4b254a4..fddfbfd9c 100644 --- a/server/runtime/storage/daqstorage.js +++ b/server/runtime/storage/daqstorage.js @@ -21,6 +21,7 @@ var logger; var daqDB = {}; // list of daqDB node: SQlite one pro device, influxDB only one var currentStorateDB; var runtime; +var questDbModule = null; function init(_settings, _log, _runtime) { settings = _settings; @@ -42,7 +43,7 @@ function reset() { function addDaqNode(_id, fncgetprop) { var id = _id; const dbType = _getDbType(); - if (dbType === DaqStoreTypeEnum.influxDB || dbType === DaqStoreTypeEnum.influxDB18 || dbType === DaqStoreTypeEnum.TDengine) { + if (dbType === DaqStoreTypeEnum.influxDB || dbType === DaqStoreTypeEnum.influxDB18 || dbType === DaqStoreTypeEnum.TDengine || dbType === DaqStoreTypeEnum.questDB) { id = dbType; } if (!daqDB[id]) { @@ -50,6 +51,14 @@ function addDaqNode(_id, fncgetprop) { daqDB[id] = InfluxDB.create(settings, logger, currentStorateDB); } else if(id === DaqStoreTypeEnum.TDengine){ daqDB[id] = TDengine.create(settings, logger, currentStorateDB); + } else if(id === DaqStoreTypeEnum.questDB){ + const QuestDB = _getQuestDbModule(); + if (QuestDB) { + daqDB[id] = QuestDB.create(settings, logger, currentStorateDB); + } else { + logger.warn('daqstorage: QuestDB storage selected but package dependencies are not installed. Falling back to SQLite.'); + daqDB[id] = SqliteDB.create(settings, logger, id, currentStorateDB); + } } else { daqDB[id] = SqliteDB.create(settings, logger, id, currentStorateDB); } @@ -160,16 +169,33 @@ function _getDaqNode(tagid) { function _getDbType() { if (settings.daqstore && settings.daqstore.type) { + if (settings.daqstore.type === 'QuestDB') { + return DaqStoreTypeEnum.questDB; + } return settings.daqstore.type; } return DaqStoreTypeEnum.SQlite; } +function _getQuestDbModule() { + if (questDbModule) { + return questDbModule; + } + try { + questDbModule = require("./questdb"); + } catch (err) { + questDbModule = null; + logger.warn(`daqstorage: QuestDB module unavailable (${err.message})`); + } + return questDbModule; +} + var DaqStoreTypeEnum = { SQlite: 'SQlite', influxDB: 'influxDB', influxDB18: 'influxDB18', TDengine: 'TDengine', + questDB: 'questDB', } function _getValue(value) { @@ -187,4 +213,4 @@ module.exports = { getNodesValues: getNodesValues, checkRetention: checkRetention, getCurrentStorageFnc: getCurrentStorageFnc, -}; \ No newline at end of file +}; diff --git a/server/runtime/storage/questdb/index.js b/server/runtime/storage/questdb/index.js new file mode 100644 index 000000000..2cad732cc --- /dev/null +++ b/server/runtime/storage/questdb/index.js @@ -0,0 +1,224 @@ +'use strict' + +const { Pool } = require('pg'); +const { Sender } = require('@questdb/nodejs-client'); +let utils = require('../../utils'); + +function QuestDB(_settings, _log, _currentStorage) { + let settings = _settings; // Application settings + const logger = _log; // Application logger + const currentStorage = _currentStorage; // Database to set the last value (current) + let pool = null; + let sender = null; + const tableName = getTableName(); + let writeQueue = Promise.resolve(); + let initPromise = Promise.resolve(); + + this.setCall = function (_fncGetProp) { + fncGetTagProp = _fncGetProp; + return this.addDaqValues; + } + var fncGetTagProp = null; + + this.init = async function () { + try { + pool = new Pool(getQueryClientConfig()); + sender = await Sender.fromConfig(getIngestConfigString()); + await ensureSchema(); + logger.info('QuestDB connected'); + } catch (error) { + logger.error(`questdb-init failed! ${error}`); + } + } + + this.addDaqValues = function (tagsValues, deviceName, deviceId) { + var dataToRestore = []; + var rowsToWrite = []; + + for (const tagid in tagsValues) { + let tag = tagsValues[tagid]; + if (!tag.daq || utils.isNullOrUndefined(tag.value) || Number.isNaN(tag.value)) { + if (tag.daq && tag.daq.restored) { + dataToRestore.push({ id: tag.id, deviceId: deviceId, value: tag.value }); + } + if (tag.daq && !tag.daq.enabled) { + continue; + } + } + + rowsToWrite.push({ + tagid, + deviceId, + deviceName: deviceName || '', + unsPath: normalizeUnsPath(tag.unsPath), + value: tag.value, + timestamp: tag.timestamp || Date.now(), + }); + } + + if (rowsToWrite.length) { + writeQueue = writeQueue.then(async () => { + await initPromise; + if (!sender) { + return; + } + + for (const row of rowsToWrite) { + const parsedValue = normalizeValue(row.value); + let line = sender + .table(tableName) + .symbol('tag_id', row.tagid) + .symbol('device_id', row.deviceId) + .stringColumn('device_name', row.deviceName); + if (!utils.isNullOrUndefined(row.unsPath)) { + line = line.stringColumn('uns_path', row.unsPath); + } + + if (!utils.isNullOrUndefined(parsedValue.numberValue)) { + line = line.floatColumn('number_value', parsedValue.numberValue); + } + if (!utils.isNullOrUndefined(parsedValue.stringValue)) { + line = line.stringColumn('string_value', parsedValue.stringValue); + } + await line.at(Number(row.timestamp), 'ms'); + } + await sender.flush(); + }).catch((error) => { + logger.error(`questdb-addDaqValues failed! ${error}`); + }); + } + + if (dataToRestore.length && currentStorage) { + currentStorage.setValues(dataToRestore); + } + } + + this.getDaqValue = function (tagid, fromts, tots) { + return new Promise(function (resolve, reject) { + initPromise.then(() => { + if (!pool) { + resolve([]); + return; + } + + const query = `SELECT timestamp, number_value, string_value + FROM ${tableName} + WHERE tag_id = $1 + AND timestamp >= $2 + AND timestamp < $3 + ORDER BY timestamp`; + const params = [tagid, new Date(fromts), new Date(tots)]; + + pool.query(query, params) + .then((result) => { + let data = []; + result.rows.forEach((row) => { + const value = !utils.isNullOrUndefined(row.number_value) ? Number(row.number_value) : row.string_value; + data.push({ dt: new Date(row.timestamp).getTime(), value }); + }); + resolve(data) + }) + .catch((error) => { + logger.error(`questdb-getDaqValue failed! ${error}`) + reject(error) + }) + }).catch((error) => { + logger.error(`questdb-getDaqValue failed! ${error}`) + reject(error) + }); + }) + } + + this.close = function () { + if (sender) { + sender.close().catch((error) => { + logger.error(`questdb-close sender failed! ${error}`); + }); + sender = null; + } + if (pool) { + pool.end().catch((error) => { + logger.error(`questdb-close pool failed! ${error}`); + }); + pool = null; + } + } + + this.getDaqMap = function (tagid) { + var dummy = {}; + dummy[tagid] = true; + return dummy; + } + + async function ensureSchema() { + if (!pool) { + return; + } + await pool.query(`CREATE TABLE IF NOT EXISTS ${tableName} ( + timestamp TIMESTAMP, + tag_id SYMBOL, + number_value FLOAT, + string_value STRING, + device_id SYMBOL, + device_name STRING, + uns_path SYMBOL + ) TIMESTAMP(timestamp) PARTITION BY DAY`); + } + + function getIngestConfigString() { + return settings.daqstore.configurationString || 'http::addr=localhost:9000;'; + } + + function getQueryClientConfig() { + return { + host: settings.daqstore.host || '127.0.0.1', + port: 8812, // Standard port + database: 'qdb', // Standard database + user: settings.daqstore.credentials?.username || 'admin', + password: settings.daqstore.credentials?.password || 'quest', + max: 10, // Pool with 10 connections + idleTimeoutMillis: 30000, // 30s + }; + } + + function getTableName() { + const name = (settings.daqstore.tableName || 'meters').trim(); + if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) { + return name.toLowerCase(); + } + logger.warn(`questdb invalid tableName "${name}", fallback to meters`); + return 'meters'; + } + + function normalizeValue(value) { + if (utils.isNullOrUndefined(value)) { + return { numberValue: null, stringValue: null }; + } + if (utils.isBoolean(value)) { + return { + numberValue: value ? 1 : 0, + stringValue: null + }; + } + if (typeof value === 'number' && Number.isFinite(value)) { + return { numberValue: value, stringValue: null }; + } + return { numberValue: null, stringValue: String(value) }; + } + + function normalizeUnsPath(value) { + if (utils.isNullOrUndefined(value)) { + return null; + } + const normalized = String(value).trim(); + return normalized.length ? normalized : null; + } + + initPromise = this.init(); +} + +module.exports = { + create: function (data, logger, currentStorage) { + return new QuestDB(data, logger, currentStorage); + } +};