From 4c5578650e072132d451cde6e3cb1c5b0610a0b4 Mon Sep 17 00:00:00 2001 From: Ralf Aron Date: Sun, 21 Jul 2024 21:53:57 +0200 Subject: [PATCH] fix: add AASApiClientV1 --- .../aas-provider/aas-resource-scan-factory.ts | 4 + .../src/app/packages/aas-resource-factory.ts | 6 + .../packages/aas-server/aas-api-client-v1.ts | 271 ++++++++++++++++++ 3 files changed, 281 insertions(+) create mode 100644 projects/aas-server/src/app/packages/aas-server/aas-api-client-v1.ts diff --git a/projects/aas-server/src/app/aas-provider/aas-resource-scan-factory.ts b/projects/aas-server/src/app/aas-provider/aas-resource-scan-factory.ts index 91d72e8b..70b2a6df 100644 --- a/projects/aas-server/src/app/aas-provider/aas-resource-scan-factory.ts +++ b/projects/aas-server/src/app/aas-provider/aas-resource-scan-factory.ts @@ -17,6 +17,7 @@ import { OpcuaClient } from '../packages/opcua/opcua-client.js'; import { AasxDirectory } from '../packages/file-system/aasx-directory.js'; import { AASApiClient } from '../packages/aas-server/aas-api-client.js'; import { AASApiClientV3 } from '../packages/aas-server/aas-api-client-v3.js'; +import { AASApiClientV1 } from '../packages/aas-server/aas-api-client-v1.js'; import { AASApiClientV0 } from '../packages/aas-server/aas-api-client-v0.js'; import { FileStorageProvider } from '../file-storage/file-storage-provider.js'; @@ -35,6 +36,9 @@ export class AASResourceScanFactory { case 'v0': source = new AASApiClientV0(this.logger, endpoint.url, endpoint.name); break; + case 'v1': + source = new AASApiClientV1(this.logger, endpoint.url, endpoint.name); + break; case 'v3': source = new AASApiClientV3(this.logger, endpoint.url, endpoint.name); break; diff --git a/projects/aas-server/src/app/packages/aas-resource-factory.ts b/projects/aas-server/src/app/packages/aas-resource-factory.ts index f7f9f0e4..b1c54f7c 100644 --- a/projects/aas-server/src/app/packages/aas-resource-factory.ts +++ b/projects/aas-server/src/app/packages/aas-resource-factory.ts @@ -16,6 +16,7 @@ import { AASApiClientV3 } from './aas-server/aas-api-client-v3.js'; import { OpcuaClient } from './opcua/opcua-client.js'; import { ERRORS } from '../errors.js'; import { FileStorageProvider } from '../file-storage/file-storage-provider.js'; +import { AASApiClientV1 } from './aas-server/aas-api-client-v1.js'; @singleton() export class AASResourceFactory { @@ -35,6 +36,8 @@ export class AASResourceFactory { switch (endpoint.version) { case 'v3': return new AASApiClientV3(this.logger, endpoint.url, endpoint.name); + case 'v1': + return new AASApiClientV1(this.logger, endpoint.url, endpoint.name); case 'v0': return new AASApiClientV0(this.logger, endpoint.url, endpoint.name); default: @@ -69,6 +72,9 @@ export class AASResourceFactory { case 'v3': await new AASApiClientV3(this.logger, endpoint.url, endpoint.name).testAsync(); break; + case 'v1': + await new AASApiClientV1(this.logger, endpoint.url, endpoint.name).testAsync(); + break; case 'v0': await new AASApiClientV0(this.logger, endpoint.url, endpoint.name).testAsync(); break; diff --git a/projects/aas-server/src/app/packages/aas-server/aas-api-client-v1.ts b/projects/aas-server/src/app/packages/aas-server/aas-api-client-v1.ts new file mode 100644 index 00000000..708ca9a1 --- /dev/null +++ b/projects/aas-server/src/app/packages/aas-server/aas-api-client-v1.ts @@ -0,0 +1,271 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2024 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import FormData from 'form-data'; +import { createReadStream } from 'fs'; +import { + aas, + DifferenceItem, + getIdShortPath, + isAssetAdministrationShell, + isSubmodel, + isSubmodelElement, + selectSubmodel, +} from 'aas-core'; + +import { encodeBase64Url } from '../../convert.js'; +import { AASApiClient } from './aas-api-client.js'; +import { Logger } from '../../logging/logger.js'; +import { JsonWriter } from '../json-writer.js'; +import * as aasv2 from '../../types/aas-v2.js'; +import { JsonReaderV2 } from '../json-reader-v2.js'; + +interface PackageDescriptor { + aasIds: string[]; + packageId: string; +} + +export interface Message { + code?: string; + correlationId?: string; + messageType: 'Undefined' | 'Info' | 'Warning' | 'Error' | 'Exception'; + text: string; + timeStamp?: string; +} + +export interface OperationResult { + messages?: Message[]; + executionState: 'Initiated' | 'Running' | 'Completed' | 'Canceled' | 'Failed' | 'Timeout'; + success: boolean; + outputVariables?: aasv2.OperationVariable[]; + inoutputVariables?: aasv2.OperationVariable[]; +} + +export class AASApiClientV1 extends AASApiClient { + public constructor(logger: Logger, url: string, name: string) { + super(logger, url, name); + } + + public override readonly version = 'v3'; + + public readonly readOnly = false; + + public readonly onlineReady = true; + + public async getShellsAsync(): Promise { + const result = await this.message.get(this.resolve('shells')); + return result.map(shell => shell.identification.id); + } + + public async readEnvironmentAsync(id: string): Promise { + const aasId = encodeBase64Url(id); + const shell = await this.message.get(this.resolve(`shells/${aasId}`)); + const submodels: aasv2.Submodel[] = []; + if (shell.submodels) { + for (const reference of shell.submodels) { + const submodelId = encodeBase64Url(reference.keys[0].value); + try { + submodels.push(await this.message.get(this.resolve(`submodels/${submodelId}`))); + } catch (error) { + this.logger.error(`Unable to read Submodel "${reference.keys[0].value}": ${error?.message}`); + } + } + } + + const result = await this.message.get(this.resolve(`concept-descriptions`)); + const asset: aasv2.Asset = { + kind: 'Instance', + identification: { idType: shell.asset.keys[0].idType, id: shell.asset.keys[0].value }, + modelType: { name: 'Asset' }, + idShort: 'Asset', + }; + + const sourceEnv: aasv2.AssetAdministrationShellEnvironment = { + assetAdministrationShells: [shell], + assets: [asset], + submodels, + conceptDescriptions: result, + }; + + return new JsonReaderV2(sourceEnv).readEnvironment(); + } + + public override getThumbnailAsync(id: string): Promise { + return this.message.getResponse(this.resolve(`shells/${encodeBase64Url(id)}/asset-information/thumbnail`)); + } + + public async commitAsync( + source: aas.Environment, + target: aas.Environment, + diffs: DifferenceItem[], + ): Promise { + const messages: string[] = []; + const aasId = encodeBase64Url(target.assetAdministrationShells[0].id); + for (const diff of diffs) { + if (diff.type === 'inserted') { + if (isSubmodel(diff.sourceElement)) { + messages.push(await this.postSubmodelAsync(aasId, diff.sourceElement)); + } else if (isSubmodelElement(diff.sourceElement)) { + const submodel = this.getSubmodel(target, diff.destinationParent); + messages.push(await this.postSubmodelElementAsync(submodel, diff.sourceElement)); + } else { + throw new Error(`Inserting "${diff?.sourceElement?.modelType}" is not implemented.`); + } + } else if (diff.type === 'changed') { + if (isSubmodel(diff.sourceElement)) { + messages.push(await this.putSubmodelAsync(aasId, diff.sourceElement)); + } else if (isSubmodelElement(diff.sourceElement)) { + const submodel = this.getSubmodel(target, diff.destinationElement); + messages.push( + await this.putSubmodelElementAsync(submodel, diff.sourceElement as aas.SubmodelElement), + ); + } else if (isAssetAdministrationShell(diff.sourceElement)) { + messages.push(await this.putShellAsync(diff.sourceElement)); + } else { + throw new Error(`Updating "${diff?.sourceElement?.modelType}" is not implemented.`); + } + } else if (diff.type === 'deleted') { + if (isSubmodel(diff.destinationElement)) { + messages.push(await this.deleteSubmodelAsync(diff.destinationElement.id)); + } else if (isSubmodelElement(diff.destinationElement)) { + const submodel = this.getSubmodel(target, diff.destinationParent); + messages.push(await this.deleteSubmodelElementAsync(submodel, diff.destinationElement)); + } else { + throw new Error(`Deleting "${diff?.destinationElement?.modelType}" is not implemented.`); + } + } + } + + return messages; + } + + public async openFileAsync(_: aas.AssetAdministrationShell, file: aas.File): Promise { + const smId = encodeBase64Url(file.parent!.keys[0].value); + const path = getIdShortPath(file); + const url = this.resolve(`submodels/${smId}/submodel-elements/${path}/attachment`); + return await this.message.getResponse(url); + } + + public resolveNodeId(shell: aas.AssetAdministrationShell, nodeId: string): string { + const aasId = encodeBase64Url(shell.id); + const items = nodeId.split('.'); + const path = items[1].split('/').slice(1).join('.'); + return this.resolve(`shells/${aasId}/submodels/${items[0]}/submodel-elements/${path}`).href; + } + + public async getPackageAsync(aasIdentifier: string): Promise { + const aasId = encodeBase64Url(aasIdentifier); + const descriptors: PackageDescriptor[] = await this.message.get(this.resolve(`packages?aasId=${aasId}`)); + const packageId = encodeBase64Url(descriptors[0].packageId); + return await this.message.getResponse(this.resolve(`packages/${packageId}`)); + } + + public postPackageAsync(file: Express.Multer.File): Promise { + const formData = new FormData(); + formData.append('file', createReadStream(file.path)); + formData.append('fileName', file.filename); + return this.message.post(this.resolve(`packages`), formData); + } + + public async deletePackageAsync(aasIdentifier: string): Promise { + const aasId = encodeBase64Url(aasIdentifier); + const descriptors: PackageDescriptor[] = await this.message.get(this.resolve(`packages?aasId=${aasId}`)); + const packageId = encodeBase64Url(descriptors[0].packageId); + return await this.message.delete(this.resolve(`packages/${packageId}`)); + } + + public async invoke(): Promise { + throw new Error('Not implemented.'); + } + + public async getBlobValueAsync( + env: aas.Environment, + submodelId: string, + idShortPath: string, + ): Promise { + const blob = await this.message.get( + this.resolve(`submodels/${submodelId}/submodel-elements/${idShortPath}/?extent=WithBlobValue`), + ); + + if (!blob) { + throw new Error(`Blob element "${submodelId}.${idShortPath}" does not exist.`); + } + + return blob.value; + } + + private async putShellAsync(shell: aas.AssetAdministrationShell): Promise { + const aasId = encodeBase64Url(shell.id); + return await this.message.put(this.resolve(`shells/${aasId}`), new JsonWriter().convert(shell)); + } + + private async putSubmodelAsync(aasId: string, submodel: aas.Submodel): Promise { + const smId = encodeBase64Url(submodel.id); + return await this.message.put( + this.resolve(`shells/${aasId}/submodels/${smId}`), + new JsonWriter().convert(submodel), + ); + } + + private async postSubmodelAsync(aasId: string, submodel: aas.Submodel): Promise { + return await this.message.post( + this.resolve(`submodels?aasIdentifier=${aasId}`), + new JsonWriter().convert(submodel), + ); + } + + private async deleteSubmodelAsync(smId: string): Promise { + return await this.message.delete(this.resolve(`submodels/${encodeBase64Url(smId)}`)); + } + + private async putSubmodelElementAsync( + submodel: aas.Submodel, + submodelElement: aas.SubmodelElement, + ): Promise { + const smId = encodeBase64Url(submodel.id); + const path = getIdShortPath(submodelElement); + return await this.message.put( + this.resolve(`submodels/${smId}/submodel-elements/${path}`), + new JsonWriter().convert(submodelElement), + ); + } + + private async postSubmodelElementAsync( + submodel: aas.Submodel, + submodelElement: aas.SubmodelElement, + ): Promise { + const smId = encodeBase64Url(submodel.id); + const path = getIdShortPath(submodelElement); + return await this.message.post( + this.resolve(`submodels/${smId}/submodel-elements/${path}`), + new JsonWriter().convert(submodelElement), + ); + } + + private async deleteSubmodelElementAsync( + submodel: aas.Submodel, + submodelElement: aas.SubmodelElement, + ): Promise { + const smId = encodeBase64Url(submodel.id); + const path = getIdShortPath(submodelElement); + return await this.message.delete(this.resolve(`submodels/${smId}/submodel-elements/${path}`)); + } + + private getSubmodel(env: aas.Environment, referable?: aas.Referable): aas.Submodel { + if (!referable) { + throw new Error('Argument undefined.'); + } + + const submodel = selectSubmodel(env, referable); + if (!submodel) { + throw new Error('Invalid operation.'); + } + + return submodel; + } +}