diff --git a/projects/aas-lib/src/lib/aas-table/aas-table-row.ts b/projects/aas-lib/src/lib/aas-table/aas-table-row.ts index 2036b5d5..a552d6ae 100644 --- a/projects/aas-lib/src/lib/aas-table/aas-table-row.ts +++ b/projects/aas-lib/src/lib/aas-table/aas-table-row.ts @@ -37,7 +37,7 @@ export class AASTableRow extends TreeNode { } public get thumbnail(): string { - return this.element.thumbnail ?? '/assets/resources/aas.32.png'; + return this.element.thumbnail || '/assets/resources/aas.32.png'; } public get endpoint(): string { diff --git a/projects/aas-lib/src/lib/aas-table/aas-table.component.html b/projects/aas-lib/src/lib/aas-table/aas-table.component.html index 047144a8..29f3bc43 100644 --- a/projects/aas-lib/src/lib/aas-table/aas-table.component.html +++ b/projects/aas-lib/src/lib/aas-table/aas-table.component.html @@ -37,7 +37,7 @@ @switch (row.state) { @case ('unloaded') {
- +
@@ -46,7 +46,7 @@ } @case ('unavailable') {
- +
@@ -55,7 +55,7 @@ } @default {
- +
} } @@ -77,8 +77,8 @@ - +
COLUMN_NAME
diff --git a/projects/aas-server/src/app/aas-index/mysql/mysql-index.ts b/projects/aas-server/src/app/aas-index/mysql/mysql-index.ts index d67b4350..a4e3a531 100644 --- a/projects/aas-server/src/app/aas-index/mysql/mysql-index.ts +++ b/projects/aas-server/src/app/aas-index/mysql/mysql-index.ts @@ -85,9 +85,12 @@ export class MySqlIndex extends AASIndex { name: row.name, url: row.url, type: row.type, - version: row.version, }; + if (row.version) { + endpoint.version = row.version; + } + if (row.headers) { endpoint.headers = JSON.parse(row.headers); } @@ -270,17 +273,8 @@ export class MySqlIndex extends AASIndex { const uuid = result[0][0].uuid; await connection.query( - 'UPDATE `documents` SET address = ?, crc32 = ?, idShort = ?, onlineReady = ?, readonly = ?, timestamp = ?, thumbnail = ? WHERE uuid = ?;', - [ - document.address, - document.crc32, - document.idShort, - !!document.onlineReady, - document.readonly, - document.timestamp, - document.thumbnail, - uuid, - ], + 'UPDATE `documents` SET address = ?, crc32 = ?, idShort = ?, timestamp = ?, thumbnail = ? WHERE uuid = ?;', + [document.address, document.crc32, document.idShort, document.timestamp, document.thumbnail, uuid], ); if (document.content) { @@ -301,7 +295,7 @@ export class MySqlIndex extends AASIndex { await connection.beginTransaction(); const uuid = v4(); await connection.query( - 'INSERT INTO `documents` (uuid, address, crc32, endpoint, id, idShort, assetId, onlineReady, readonly, thumbnail, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);', + 'INSERT INTO `documents` (uuid, address, crc32, endpoint, id, idShort, assetId, thumbnail, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);', [ uuid, document.address, @@ -309,10 +303,8 @@ export class MySqlIndex extends AASIndex { document.endpoint, document.id, document.idShort, - document.assetId, - !!document.onlineReady, - document.readonly, - document.thumbnail ?? '', + document.assetId || null, + document.thumbnail || null, BigInt(document.timestamp), ], ); @@ -582,19 +574,27 @@ export class MySqlIndex extends AASIndex { } private toDocument(result: MySqlDocument): AASDocument { - return { + const document: AASDocument = { address: result.address, crc32: result.crc32, endpoint: result.endpoint, id: result.id, idShort: result.idShort, - assetId: result.assetId, - readonly: result.readonly ? true : false, timestamp: Number(result.timestamp), content: null, - onlineReady: result.onlineReady ? true : false, - thumbnail: result.thumbnail, + onlineReady: true, + readonly: true, }; + + if (result.assetId) { + document.assetId = result.assetId; + } + + if (result.thumbnail) { + document.thumbnail = result.thumbnail; + } + + return document; } private async initialize(): Promise { diff --git a/projects/aas-server/src/app/aas-index/mysql/mysql-types.ts b/projects/aas-server/src/app/aas-index/mysql/mysql-types.ts index 03261e91..b02c71c3 100644 --- a/projects/aas-server/src/app/aas-index/mysql/mysql-types.ts +++ b/projects/aas-server/src/app/aas-index/mysql/mysql-types.ts @@ -7,19 +7,25 @@ *****************************************************************************/ import { RowDataPacket } from 'mysql2/promise'; -import { AASDocument, AASEndpointType } from 'aas-core'; +import { AASEndpointType } from 'aas-core'; export interface MySqlEndpoint extends RowDataPacket { name: string; url: string; type: AASEndpointType; - version?: string; - headers?: string; - schedule?: string; + version: string | null; + headers: string | null; + schedule: string | null; } -export interface MySqlDocument extends AASDocument, RowDataPacket { +export interface MySqlDocument extends RowDataPacket { uuid: string; + address: string; + crc32: number; + idShort: string; + assetId: string | null; + thumbnail: string | null; + timestamp: number; } export interface MySqlElement extends RowDataPacket { diff --git a/projects/aas-server/src/app/aas-provider/aas-provider.ts b/projects/aas-server/src/app/aas-provider/aas-provider.ts index 4f909999..6d0dc0b2 100644 --- a/projects/aas-server/src/app/aas-provider/aas-provider.ts +++ b/projects/aas-server/src/app/aas-provider/aas-provider.ts @@ -38,7 +38,7 @@ import { AASResourceFactory } from '../packages/aas-resource-factory.js'; import { Variable } from '../variable.js'; import { WSServer } from '../ws-server.js'; import { ERRORS } from '../errors.js'; -import { TaskHandler } from './task-handler.js'; +import { Task, TaskHandler } from './task-handler.js'; import { HierarchicalStructure } from './hierarchical-structure.js'; import { AASCache } from './aas-cache.js'; @@ -237,7 +237,8 @@ export class AASProvider { } as AASServerMessage, }); - setTimeout(this.scanEndpoint, 0, this.taskHandler.createTaskId(), endpoint); + const task = this.taskHandler.createTask(endpointName, this, 'ScanEndpoint'); + setTimeout(this.scanEndpoint, 0, task, endpoint); } /** @@ -261,6 +262,11 @@ export class AASProvider { const endpoint = await this.index.getEndpoint(endpointName); if (endpoint) { await this.index.removeEndpoint(endpoint.name); + const task = this.taskHandler.find(endpointName, 'ScanEndpoint'); + if (task) { + this.taskHandler.delete(task.id); + } + this.logger.info(`Endpoint ${endpoint.name} (${endpoint.url}) removed.`); this.wsServer.notify('IndexChange', { type: 'AASServerMessage', @@ -429,6 +435,27 @@ export class AASProvider { return nodes; } + /** Starts a scan of the AAS endpoint with the specified name. + * @param name The name of the endpoint. + */ + public async startEndpointScan(name: string): Promise { + const endpoint = await this.index.getEndpoint(name); + const task = this.taskHandler.find(name, 'ScanEndpoint'); + if (task === undefined) { + throw new Error(``); + } + + if (endpoint.schedule?.type !== 'manual') { + throw new Error(`Endpoint ${name} is not configured for the manual start of a scan.`); + } + + if (task.state === 'inProgress') { + throw new Error(`Scanning endpoint ${name} is already in progress.`); + } + + setTimeout(this.scanEndpoint, 0, task, endpoint); + } + private async restart(): Promise { this.resetRequested = false; await this.index.reset(); @@ -482,14 +509,16 @@ export class AASProvider { continue; } - setTimeout(this.scanEndpoint, 0, this.taskHandler.createTaskId(), endpoint); + const task = this.taskHandler.createTask(endpoint.name, this, 'ScanEndpoint'); + this.taskHandler.set(task); + setTimeout(this.scanEndpoint, 0, task, endpoint); } } catch (error) { this.logger.error(error); } }; - private computeTimeout(schedule: AASEndpointSchedule | undefined, start?: number): number { + private computeTimeout(schedule: AASEndpointSchedule | undefined, start: number, end: number): number { if (schedule === undefined) { return this.variable.SCAN_ENDPOINT_TIMEOUT; } @@ -498,7 +527,7 @@ export class AASProvider { if (schedule.type === 'every') { const values = schedule.values; if (values && values.length > 0 && typeof values[0] === 'number') { - const timeout = Date.now() - start - values[0]; + const timeout = end - start - values[0]; return timeout >= 0 ? timeout : values[0]; } } @@ -506,15 +535,15 @@ export class AASProvider { return this.variable.SCAN_ENDPOINT_TIMEOUT; } - private scanEndpoint = async (taskId: number, endpoint: AASEndpoint) => { + private scanEndpoint = async (task: Task, endpoint: AASEndpoint) => { const data: ScanEndpointData = { type: 'ScanEndpointData', - taskId, + taskId: task.id, endpoint, - start: Date.now(), }; - this.taskHandler.set(taskId, { endpointName: endpoint.name, owner: this, type: 'ScanEndpoint' }); + task.state = 'inProgress'; + task.start = Date.now(); this.parallel.execute(data); }; @@ -544,21 +573,21 @@ export class AASProvider { private parallelOnEnd = async (result: ScanResult) => { const task = this.taskHandler.get(result.taskId); - if (!task || task.owner !== this) { + if (task === undefined || task.owner !== this) { return; } - this.taskHandler.delete(result.taskId); if ((await this.index.hasEndpoint(task.endpointName)) === true) { const endpoint = await this.index.getEndpoint(task.endpointName); - - if (endpoint.schedule?.type === 'once') { + if (endpoint.schedule?.type === 'once' || endpoint.schedule?.type === 'manual') { return; } + task.state === 'idle'; + task.end = Date.now(); setTimeout( this.scanEndpoint, - this.computeTimeout(endpoint.schedule, result.start), + this.computeTimeout(endpoint.schedule, task.start, task.end), result.taskId, endpoint, ); diff --git a/projects/aas-server/src/app/aas-provider/scan-result.ts b/projects/aas-server/src/app/aas-provider/scan-result.ts index d9a69cc8..25295ad2 100644 --- a/projects/aas-server/src/app/aas-provider/scan-result.ts +++ b/projects/aas-server/src/app/aas-provider/scan-result.ts @@ -20,7 +20,6 @@ export interface ScanResult { type: 'ScanEndResult' | 'ScanEndpointResult' | 'ScanTemplatesResult'; kind: ScanResultKind; taskId: number; - start: number; messages?: Message[]; } diff --git a/projects/aas-server/src/app/aas-provider/task-handler.ts b/projects/aas-server/src/app/aas-provider/task-handler.ts index 760ec767..b4d8d84d 100644 --- a/projects/aas-server/src/app/aas-provider/task-handler.ts +++ b/projects/aas-server/src/app/aas-provider/task-handler.ts @@ -9,9 +9,13 @@ import { singleton } from 'tsyringe'; export interface Task { + id: number; endpointName: string; owner: object; type: 'ScanEndpoint' | 'ScanTemplates'; + state: 'idle' | 'inProgress'; + start: number; + end: number; } @singleton() @@ -28,8 +32,8 @@ export class TaskHandler { return this.tasks.get(taskId); } - public set(taskId: number, task: Task) { - this.tasks.set(taskId, task); + public set(task: Task) { + this.tasks.set(task.id, task); } public empty(owner: object, name?: string): boolean { @@ -42,9 +46,27 @@ export class TaskHandler { return true; } - public createTaskId(): number { - const taskId = this.nextTaskId; + public createTask(endpointName: string, owner: object, type: 'ScanEndpoint' | 'ScanTemplates'): Task { + const id = this.nextTaskId; ++this.nextTaskId; - return taskId; + return { + id, + type, + endpointName, + owner, + state: 'idle', + start: 0, + end: 0, + }; + } + + public find(endpointName: string, type: 'ScanEndpoint' | 'ScanTemplates'): Task | undefined { + for (const task of this.tasks.values()) { + if (task.endpointName === endpointName && type === task.type) { + return task; + } + } + + return undefined; } } diff --git a/projects/aas-server/src/app/aas-provider/worker-data.ts b/projects/aas-server/src/app/aas-provider/worker-data.ts index a2f2592b..47bff72f 100644 --- a/projects/aas-server/src/app/aas-provider/worker-data.ts +++ b/projects/aas-server/src/app/aas-provider/worker-data.ts @@ -11,7 +11,6 @@ import { AASEndpoint } from 'aas-core'; export interface WorkerData { taskId: number; type: 'ScanEndpointData' | 'ScanTemplatesData'; - start: number; } export interface ScanEndpointData extends WorkerData { diff --git a/projects/aas-server/src/app/controller/endpoints-controller.ts b/projects/aas-server/src/app/controller/endpoints-controller.ts index ab21259f..e55a31f7 100644 --- a/projects/aas-server/src/app/controller/endpoints-controller.ts +++ b/projects/aas-server/src/app/controller/endpoints-controller.ts @@ -143,4 +143,20 @@ export class EndpointsController extends AASController { this.logger.stop(); } } + + /** + * @summary Starts a scan of the AAS endpoint with the specified name. + * @param name The endpoint name. + */ + @Post('{name}/scan') + @Security('bearerAuth', ['editor']) + @OperationId('reset') + public async startEndpointScan(@Path() name: string): Promise { + try { + this.logger.start('startEndpointScan'); + await this.aasProvider.startEndpointScan(decodeBase64Url(name)); + } finally { + this.logger.stop(); + } + } } diff --git a/projects/aas-server/src/app/endpoint-scan.ts b/projects/aas-server/src/app/endpoint-scan.ts index 15ec37b1..2bf686c5 100644 --- a/projects/aas-server/src/app/endpoint-scan.ts +++ b/projects/aas-server/src/app/endpoint-scan.ts @@ -62,7 +62,6 @@ export class EndpointScan { kind: ScanResultKind.Update, endpoint: this.data.endpoint, document: document, - start: this.data.start, }; const array = toUint8Array(value); @@ -76,7 +75,6 @@ export class EndpointScan { kind: ScanResultKind.Remove, endpoint: this.data.endpoint, document: document, - start: this.data.start, }; const array = toUint8Array(value); @@ -90,7 +88,6 @@ export class EndpointScan { kind: ScanResultKind.Add, endpoint: this.data.endpoint, document: document, - start: this.data.start, }; const array = toUint8Array(value); diff --git a/projects/aas-server/src/app/template/template-scan.ts b/projects/aas-server/src/app/template/template-scan.ts index ac32c21c..f482e96c 100644 --- a/projects/aas-server/src/app/template/template-scan.ts +++ b/projects/aas-server/src/app/template/template-scan.ts @@ -53,7 +53,6 @@ export class TemplateScan { taskId: data.taskId, kind: ScanResultKind.Update, templates: templates, - start: data.start, }; const array = toUint8Array(value); diff --git a/projects/aas-server/src/app/template/template-storage.ts b/projects/aas-server/src/app/template/template-storage.ts index 07b80eff..b62a9380 100644 --- a/projects/aas-server/src/app/template/template-storage.ts +++ b/projects/aas-server/src/app/template/template-storage.ts @@ -19,7 +19,7 @@ import { AasxDirectory } from '../packages/file-system/aasx-directory.js'; import { ScanTemplatesData } from '../aas-provider/worker-data.js'; import { ScanResult, ScanTemplatesResult } from '../aas-provider/scan-result.js'; import { Parallel } from '../aas-provider/parallel.js'; -import { TaskHandler } from '../aas-provider/task-handler.js'; +import { Task, TaskHandler } from '../aas-provider/task-handler.js'; @singleton() export class TemplateStorage { @@ -72,17 +72,19 @@ export class TemplateStorage { } private startScan = () => { - this.scanTemplates(this.taskHandler.createTaskId()); + const task = this.taskHandler.createTask('TemplateStorage', this, 'ScanTemplates'); + this.taskHandler.set(task); + this.scanTemplates(task); }; - private scanTemplates = async (taskId: number) => { + private scanTemplates = async (task: Task) => { const data: ScanTemplatesData = { type: 'ScanTemplatesData', - taskId, - start: Date.now(), + taskId: task.id, }; - this.taskHandler.set(taskId, { endpointName: 'TemplateStorage', owner: this, type: 'ScanTemplates' }); + task.start = Date.now(); + task.state = 'inProgress'; this.parallel.execute(data); }; @@ -120,11 +122,12 @@ export class TemplateStorage { private parallelOnEnd = (result: ScanResult) => { const task = this.taskHandler.get(result.taskId); - if (!task || task.owner !== this) { + if (task === undefined || task.owner !== this) { return; } - this.taskHandler.delete(result.taskId); + task.state = 'idle'; + task.end = Date.now(); setTimeout(this.scanTemplates, this.timeout, result.taskId); if (result.messages) { diff --git a/projects/aas-server/src/app/worker-app.ts b/projects/aas-server/src/app/worker-app.ts index 63c920a3..04bb22b7 100644 --- a/projects/aas-server/src/app/worker-app.ts +++ b/projects/aas-server/src/app/worker-app.ts @@ -52,7 +52,6 @@ export class WorkerApp { type: 'ScanEndResult', taskId: data.taskId, kind: ScanResultKind.End, - start: data.start, messages: this.logger.getMessages(), }; } diff --git a/projects/aas-server/src/test/aas-index/mysql/mysql-index.spec.ts b/projects/aas-server/src/test/aas-index/mysql/mysql-index.spec.ts index c7fac5a2..ffe78f0d 100644 --- a/projects/aas-server/src/test/aas-index/mysql/mysql-index.spec.ts +++ b/projects/aas-server/src/test/aas-index/mysql/mysql-index.spec.ts @@ -69,19 +69,35 @@ describe('MySqlIndex', () => { name: 'Endpoint 1', url: 'http://endpoint1.com', type: 'AAS_API', + version: 'v3', + headers: null, + schedule: null, }, { constructor: { name: 'RowDataPacket' }, name: 'Endpoint 2', url: 'http://endpoint2.com', type: 'AAS_API', + version: 'v3', + headers: null, + schedule: null, }, ]; connection.query.mockResolvedValue([result, []]); await expect(index.getEndpoints()).resolves.toEqual([ - { name: 'Endpoint 1', url: 'http://endpoint1.com', type: 'AAS_API' }, - { name: 'Endpoint 2', url: 'http://endpoint2.com', type: 'AAS_API' }, + { + name: 'Endpoint 1', + url: 'http://endpoint1.com', + type: 'AAS_API', + version: 'v3', + }, + { + name: 'Endpoint 2', + url: 'http://endpoint2.com', + type: 'AAS_API', + version: 'v3', + }, ]); expect(connection.query).toBeCalledWith('SELECT * FROM `endpoints`;'); @@ -95,12 +111,20 @@ describe('MySqlIndex', () => { name: 'Endpoint 1', url: 'http://endpoint1.com', type: 'AAS_API', + version: 'v3', + headers: null, + schedule: null, }; connection.query.mockResolvedValue([[result], []]); const actual = await index.getEndpoint('Endpoint 1'); expect(connection.query).toBeCalledWith('SELECT * FROM `endpoints` WHERE name = ?;', ['Endpoint 1']); - expect(actual).toEqual({ name: 'Endpoint 1', url: 'http://endpoint1.com', type: 'AAS_API' }); + expect(actual).toEqual({ + name: 'Endpoint 1', + url: 'http://endpoint1.com', + type: 'AAS_API', + version: 'v3', + }); }); it('throws an error if endpoint does not exist', async () => { @@ -117,6 +141,9 @@ describe('MySqlIndex', () => { name: 'Endpoint 1', url: 'http://endpoint1.com', type: 'AAS_API', + version: 'v3', + headers: null, + schedule: null, }; connection.query.mockResolvedValue([[result], []]); @@ -247,7 +274,8 @@ describe('MySqlIndex', () => { address: '', crc32: 0, idShort: 'Shell 1', - readonly: false, + assetId: null, + thumbnail: null, timestamp: 0, id: 'http://document1/aas', endpoint: 'Endpoint 1', @@ -258,7 +286,8 @@ describe('MySqlIndex', () => { address: '', crc32: 0, idShort: 'Shell 2', - readonly: false, + assetId: null, + thumbnail: null, timestamp: 0, id: 'http://document2/aas', endpoint: 'Endpoint 1', diff --git a/projects/aas-server/src/test/template/template-storage.spec.ts b/projects/aas-server/src/test/template/template-storage.spec.ts index 1b55338d..4855d616 100644 --- a/projects/aas-server/src/test/template/template-storage.spec.ts +++ b/projects/aas-server/src/test/template/template-storage.spec.ts @@ -19,7 +19,7 @@ import { TaskHandler } from '../../app/aas-provider/task-handler.js'; import { Parallel } from '../../app/aas-provider/parallel.js'; import { ScanResultKind, ScanTemplatesResult } from '../../app/aas-provider/scan-result.js'; -describe('TemplateStorage', function () { +describe('TemplateStorage', () => { let templateStorage: TemplateStorage; let logger: jest.Mocked; let fileStorage: jest.Mocked; @@ -27,7 +27,7 @@ describe('TemplateStorage', function () { let variable: jest.Mocked; let parallel: jest.Mocked; - beforeEach(function () { + beforeEach(() => { logger = createSpyObj(['error']); fileStorageProvider = createSpyObj(['get']); fileStorage = createSpyObj(['exists', 'readDir', 'readFile']); @@ -40,7 +40,6 @@ describe('TemplateStorage', function () { type: 'ScanTemplatesResult', taskId: 1, kind: ScanResultKind.Update, - start: 0, templates: [ { idShort: 'aSubmodel', @@ -61,12 +60,12 @@ describe('TemplateStorage', function () { templateStorage = new TemplateStorage(logger, variable, fileStorageProvider, parallel, new TaskHandler()); }); - it('should create', function () { + it('should create', () => { expect(templateStorage).toBeTruthy(); }); - describe('getTemplatesAsync', function () { - it('gets all available templates', async function () { + describe('getTemplatesAsync', () => { + it('gets all available templates', async () => { await expect(templateStorage.getTemplatesAsync()).resolves.toEqual([ { idShort: 'aSubmodel', @@ -79,10 +78,10 @@ describe('TemplateStorage', function () { }); }); - describe('readTemplateAsync', function () { + describe('readTemplateAsync', () => { let submodel: aas.Submodel; - beforeEach(function () { + beforeEach(() => { submodel = { id: 'http://aas/submodel', idShort: 'aSubmodel', @@ -91,7 +90,7 @@ describe('TemplateStorage', function () { }; }); - it('reads the template with the address "submodel.json"', async function () { + it('reads the template with the address "submodel.json"', async () => { fileStorage.readFile.mockResolvedValue(Buffer.from(JSON.stringify(submodel))); await expect(templateStorage.readTemplateAsync('submodel.json')).resolves.toEqual(submodel); expect(fileStorage.readFile).toHaveBeenCalledWith('/templates/submodel.json'); diff --git a/projects/aasportal-index/schema.sql b/projects/aasportal-index/schema.sql index 6b95a995..37589a1b 100644 --- a/projects/aasportal-index/schema.sql +++ b/projects/aasportal-index/schema.sql @@ -17,8 +17,6 @@ CREATE TABLE documents ( id VARCHAR(255), idShort VARCHAR(100), assetId VARCHAR(255), - onlineReady BOOL, - readonly BOOL, thumbnail VARCHAR(7167), timestamp LONG );