import * as ajv from "ajv" ; import fetch from "cross-fetch" ; import * as debug from "debug" ; import * as path from "path" ; import "url-search-params-polyfill" ; import { AgentAuth } from "./agent-auth" ; import { BaseEvent , DataPointValue , DataSourceConfiguration , IMindConnectConfiguration , Mapping , TimeStampedDataPoint , } from "./mindconnect-models" ; import { bulkDataTemplate , dataTemplate } from "./mindconnect-template" ; import { dataValidator , eventValidator } from "./mindconnect-validators" ; import { MindSphereSdk } from "./sdk" ; import { fileUploadOptionalParameters , MultipartUploader } from "./sdk/common/multipart-uploader" ; import { retry , throwError } from "./utils" ; import _ = require ( "lodash" ) ; const log = debug ( "mindconnect-agent" ) ; see https export class export class MindConnectAgent extends AgentAuth { public ClientId ( ) { return this . _configuration . content . clientId || "not defined yet" ; } see https returns { boolean } memberof public IsOnBoarded ( ) { return this . _configuration . response ? true : false ; } see https returns { boolean } memberof public HasDataSourceConfiguration ( ) { if ( ! this . _configuration . dataSourceConfiguration ) { return false ; } else if ( ! this . _configuration . dataSourceConfiguration . configurationId ) { return false ; } else { return true ; } } see https returns { boolean } memberof public HasDataMappings ( ) { return ( this . _configuration . mappings || [ ] ) . length > 0 ; } param { DataSourceConfiguration } dataSourceConfiguration param { boolean } ignoreEtag returns { Promise < DataSourceConfiguration > } memberof public async PutDataSourceConfiguration ( dataSourceConfiguration , ignoreEtag = true ) { await this . RenewToken ( ) ; this . checkConfiguration ( ) ; const eTag = this . calculateEtag ( ignoreEtag , dataSourceConfiguration ) ; const agentManangement = this . Sdk ( ) . GetAgentManagementClient ( ) ; try { const storedConfig = await agentManangement . PutDataSourceConfiguration ( this . ClientId ( ) , dataSourceConfiguration , { ifMatch : eTag . toString ( ) , } ) ; this . _configuration . dataSourceConfiguration = storedConfig ; await retry ( 5 , ( ) => this . SaveConfig ( ) ) ; return storedConfig ; } catch ( err ) { log ( err ) ; throw new Error ( `Network error occured ${ err . message }` ) ; } } private calculateEtag ( ignoreEtag , dataSourceConfiguration ) { let eTag = 0 ; if ( this . _configuration . dataSourceConfiguration && this . _configuration . dataSourceConfiguration . eTag ) { eTag = parseInt ( this . _configuration . dataSourceConfiguration . eTag ) ; if ( isNaN ( eTag ) ) throw new Error ( "Invalid eTag in configuration!" ) ; } if ( ! ignoreEtag ) { if ( ! dataSourceConfiguration . eTag ) throw new Error ( "There is no eTag in the provided configuration!" ) ; eTag = parseInt ( dataSourceConfiguration . eTag ) ; if ( isNaN ( eTag ) ) throw new Error ( "Invalid eTag in provided configuration!" ) ; } return eTag ; } see https returns { Promise < DataSourceConfiguration > } memberOf public async GetDataSourceConfiguration ( ) { await this . RenewToken ( ) ; if ( ! this . _accessToken ) throw new Error ( "The agent doesn't have a valid access token." ) ; if ( ! this . _configuration . content . clientId ) throw new Error ( "No client id in the configuration of the agent." ) ; const agentManagment = this . Sdk ( ) . GetAgentManagementClient ( ) ; try { const result = await agentManagment . GetDataSourceConfiguration ( this . ClientId ( ) ) ; this . _configuration . dataSourceConfiguration = result ; await retry ( 5 , ( ) => this . SaveConfig ( ) ) ; return result ; } catch ( err ) { log ( err ) ; throw new Error ( `Network error occured ${ err . message }` ) ; } } see https returns { Promise < Array < Mapping > > } memberOf public async GetDataMappings ( ) { await this . RenewToken ( ) ; this . checkConfiguration ( ) ; const mcapi = this . Sdk ( ) . GetMindConnectApiClient ( ) ; const agentFilter = JSON . stringify ( { agentId : `${ this . _configuration . content . clientId }` } ) ; try { const result = await mcapi . GetDataPointMappings ( { size : 2000 , filter : agentFilter , } ) ; this . _configuration . mappings = result . content ; await retry ( 5 , ( ) => this . SaveConfig ( ) ) ; return result . content ; } catch ( err ) { log ( err ) ; throw new Error ( `Network error occured ${ err . message }` ) ; } } private checkConfiguration ( ) { ! this . _accessToken && throwError ( "The agent doesn't have a valid access token." ) ; ! this . _configuration . content . clientId && throwError ( "No client id in the configuration of the agent." ) ; } see https param { Mapping [ ] } mappings returns { Promise < boolean > } memberOf public async PutDataMappings ( mappings ) { await this . RenewToken ( ) ; this . checkConfiguration ( ) ; const mcapi = this . Sdk ( ) . GetMindConnectApiClient ( ) ; for ( const mapping of mappings ) { log ( `Storing mapping ${ mapping }` ) ; try { await mcapi . PostDataPointMapping ( mapping , { ignoreCodes : [ 409 ] } ) ; } catch ( err ) { log ( err ) ; throw new Error ( `Network error occured ${ err . message }` ) ; } let oldmappings = this . _configuration . mappings || [ ] ; if ( ( oldmappings as any ) . content ) { oldmappings = [ ] ; } this . _configuration . mappings = _ . uniqWith ( [ ... oldmappings , ... mappings ] , ( a , b ) => { return ( a . agentId === b . agentId && a . dataPointId === b . dataPointId && a . entityId === b . entityId && a . propertyName === b . propertyName && a . propertySetName === b . propertySetName ) ; } ) ; await retry ( 5 , ( ) => this . SaveConfig ( ) ) ; } return true ; } memberOf public async DeleteAllMappings ( ) { const toDeleteMappings = await this . GetDataMappings ( ) ; const mcapi = this . Sdk ( ) . GetMindConnectApiClient ( ) ; for ( let index = 0 ; index < toDeleteMappings . length ; index ++ ) { const element = toDeleteMappings [ index ] ; await mcapi . DeleteDataMapping ( element . id ! , { ignoreCodes : [ 404 ] } ) ; } this . _configuration . mappings = [ ] ; await retry ( 5 , ( ) => this . SaveConfig ( ) ) ; } see param { * } events param { Date } timeStamp param { boolean } validateModel returns { Promise < boolean > } memberof public async PostEvent ( event , timeStamp = new Date ( ) , validateModel = true ) { await this . RenewToken ( ) ; if ( ! this . _accessToken ) throw new Error ( "The agent doesn't have a valid access token." ) ; const eventManagement = this . Sdk ( ) . GetEventManagementClient ( ) ; const headers = { ... this . _apiHeaders , Authorization : `Bearer ${ this . _accessToken . access_token }` } ; const url = `${ this . _configuration . content . baseUrl }/api/eventmanagement/v3/events` ; log ( `GetDataSourceConfiguration Headers ${ JSON . stringify ( headers ) } Url ${ url }` ) ; if ( ! ( event as any ) . timestamp ) { ( event as any ) . timestamp = timeStamp . toISOString ( ) ; } if ( validateModel ) { const validator = this . GetEventValidator ( ) ; const isValid = await validator ( event ) ; if ( ! isValid ) { throw new Error ( `Data doesn't match the configuration! Errors: ${ JSON . stringify ( validator . errors ) }` ) ; } } await eventManagement . PostEvent ( event as any ) ; return true ; } see param { DataPointValue [ ] } dataPoints param { Date } timeStamp param { boolean } validateModel returns { Promise < boolean > } memberof public async PostData ( dataPoints , timeStamp = new Date ( ) , validateModel = true ) { await this . RenewToken ( ) ; if ( ! this . _accessToken ) throw new Error ( "The agent doesn't have a valid access token." ) ; if ( ! this . _configuration . content . clientId ) throw new Error ( "No client id in the configuration of the agent." ) ; if ( ! this . _configuration . dataSourceConfiguration ) throw new Error ( "No data source configuration for the agent." ) ; if ( ! this . _configuration . dataSourceConfiguration . configurationId ) throw new Error ( "No data source configuration ID for the agent." ) ; if ( validateModel ) { const validator = this . GetValidator ( ) ; const isValid = await validator ( dataPoints ) ; if ( ! isValid ) { throw new Error ( `Data doesn't match the configuration! Errors: ${ JSON . stringify ( validator . errors ) }` ) ; } } const headers = { ... this . _multipartHeaders , Authorization : `Bearer ${ this . _accessToken . access_token }` } ; const url = `${ this . _configuration . content . baseUrl }/api/mindconnect/v3/exchange` ; log ( `GetDataSourceConfiguration Headers ${ JSON . stringify ( headers ) } Url ${ url }` ) ; const dataMessage = dataTemplate ( timeStamp , dataPoints , this . _configuration . dataSourceConfiguration . configurationId ) ; log ( dataMessage ) ; const result = await this . SendMessage ( "POST" , url , dataMessage , headers ) ; return < boolean > result ; } param { TimeStampedDataPoint [ ] } timeStampedDataPoints param { boolean } validateModel returns { Promise < boolean > } memberOf public async BulkPostData ( timeStampedDataPoints , validateModel = true ) { await this . RenewToken ( ) ; if ( ! this . _accessToken ) throw new Error ( "The agent doesn't have a valid access token." ) ; if ( ! this . _configuration . content . clientId ) throw new Error ( "No client id in the configuration of the agent." ) ; if ( ! this . _configuration . dataSourceConfiguration ) throw new Error ( "No data source configuration for the agent." ) ; if ( ! this . _configuration . dataSourceConfiguration . configurationId ) throw new Error ( "No data source configuration ID for the agent." ) ; if ( validateModel ) { const validator = this . GetValidator ( ) ; for ( let index = 0 ; index < timeStampedDataPoints . length ; index ++ ) { const element = timeStampedDataPoints [ index ] ; const isValid = await validator ( element . values ) ; if ( ! isValid ) { throw new Error ( `Data doesn't match the configuration! Errors: ${ JSON . stringify ( validator . errors ) }` ) ; } } } const headers = { ... this . _multipartHeaders , Authorization : `Bearer ${ this . _accessToken . access_token }` } ; const url = `${ this . _configuration . content . baseUrl }/api/mindconnect/v3/exchange` ; log ( `GetDataSourceConfiguration Headers ${ JSON . stringify ( headers ) } Url ${ url }` ) ; const bulkDataMessage = bulkDataTemplate ( timeStampedDataPoints , this . _configuration . dataSourceConfiguration . configurationId ) ; log ( bulkDataMessage ) ; const result = await this . SendMessage ( "POST" , url , bulkDataMessage , headers ) ; return < boolean > result ; } param { string } entityId param { string } filepath param { ( string | Buffer ) } file param { fileUploadOptionalParameters } optional param { ( number | undefined ) } optional . part param { ( Date | undefined ) } optional . timestamp param { ( string | undefined ) } optional . description param { ( string | undefined ) } optional . type param { ( number | undefined ) } optional . chunkSize param { ( number | undefined ) } optional . retry param { ( Function | undefined ) } optional . logFunction param { ( Function | undefined ) } optional . verboseFunction param { ( boolean | undefined ) } optional . chunk param { ( number | undefined ) } optional . parallelUploads param { ( number | undefined ) } optional . ifMatch returns { Promise < string > } memberOf example example public async UploadFile ( entityId , filepath , file , optional ? ) { const result = await this . uploader . UploadFile ( entityId , filepath , file , optional ) ; await retry ( 5 , ( ) => this . SaveConfig ( ) ) ; return result ; } deprecated param { string } file param { string } fileType param { string } description param { boolean } chunk param { string } entityId param { number } chunkSize param { number } maxSockets returns { Promise < string > } memberOf public async Upload ( file , fileType , description , chunk = true , entityId ? , chunkSize = 8 * 1024 * 1024 , maxSockets = 3 , filePath ? ) { const clientId = entityId || this . ClientId ( ) ; const filepath = filePath || ( file instanceof Buffer ? "no-filepath-for-buffer" : path . basename ( file ) ) ; return await this . UploadFile ( clientId , filepath , file , { type : fileType , description : description , chunk : chunk , chunkSize : chunkSize , parallelUploads : maxSockets , } ) ; } private async SendMessage ( method , url , dataMessage , headers ) { try { const response = await fetch ( url , { method : method , body : dataMessage , headers : headers , agent : this . _proxyHttpAgent , } as RequestInit ) ; if ( ! response . ok ) { log ( { method : method , body : dataMessage , headers : headers , agent : this . _proxyHttpAgent } ) ; log ( response ) ; throw new Error ( response . statusText ) ; } const text = await response . text ( ) ; if ( response . status >= 200 && response . status <= 299 ) { const etag = response . headers . get ( "eTag" ) ; return etag ? etag : true ; } else { throw new Error ( `Error occured response status ${ response . status } ${ text }` ) ; } } catch ( err ) { log ( err ) ; throw new Error ( `Network error occured ${ err . message }` ) ; } } example param { string } assetTypeId param { ( "NUMERICAL" | "DESCRIPTIVE" ) } mode returns { Promise < DataSourceConfiguration > } memberOf public async GenerateDataSourceConfiguration ( assetTypeId , mode = "DESCRIPTIVE" ) { const assetType = await this . Sdk ( ) . GetAssetManagementClient ( ) . GetAssetType ( assetTypeId , { exploded : true } ) ; const dataSourceConfiguration = this . Sdk ( ) . GetMindConnectApiClient ( ) . GenerateDataSourceConfiguration ( assetType , mode ) ; return dataSourceConfiguration ; } example param { string } targetAssetId returns { Mapping [ ] } memberOf public GenerateMappings ( targetAssetId ) { const mcapi = this . Sdk ( ) . GetMindConnectApiClient ( ) ; ! this . _configuration . dataSourceConfiguration && throwError ( "no data source configuration! (have you forgotten to create / generate the data source configuration first?" ) ; const mappings = mcapi . GenerateMappings ( this . _configuration . dataSourceConfiguration ! , this . ClientId ( ) , targetAssetId ) ; return mappings ; } param { string } targetAssetId param { ( "NUMERICAL" | "DESCRIPTIVE" ) } mode param { boolean } overwrite memberOf public async ConfigureAgentForAssetId ( targetAssetId , mode = "DESCRIPTIVE" , overwrite = true ) { const asset = await this . Sdk ( ) . GetAssetManagementClient ( ) . GetAsset ( targetAssetId ) ; const configuration = await this . GenerateDataSourceConfiguration ( ( asset . typeId as unknown ) as string , mode ) ; if ( overwrite ) { await this . GetDataSourceConfiguration ( ) ; } await this . PutDataSourceConfiguration ( configuration , overwrite ) ; if ( overwrite ) { await this . DeleteAllMappings ( ) ; } const mappings = this . GenerateMappings ( targetAssetId ) ; await this . PutDataMappings ( mappings ) ; } private _sdk ? : MindSphereSdk = undefined ; returns { MindSphereSdk } memberOf public Sdk ( ) { if ( ! this . _sdk ) { this . _sdk = new MindSphereSdk ( this ) ; } return this . _sdk ; } returns { ajv . ValidateFunction } memberOf public GetValidator ( ) { const model = this . _configuration . dataSourceConfiguration ; if ( ! model ) { throw new Error ( "Invalid local configuration, Please get or crete the data source configuration." ) ; } return dataValidator ( model ) ; } private _eventValidator ? : ajv . ValidateFunction ; returns { ajv . ValidateFunction } memberOf public GetEventValidator ( ) { if ( ! this . _eventValidator ) { this . _eventValidator = eventValidator ( ) ; } return this . _eventValidator ; } see https returns { IMindConnectConfiguration } memberOf public GetMindConnectConfiguration ( ) { return this . _configuration ; } private uploader = new MultipartUploader ( this ) ; }