diff --git a/doc/2/concepts/models/index.md b/doc/2/concepts/models/index.md index 72311ce8..13c26cb3 100644 --- a/doc/2/concepts/models/index.md +++ b/doc/2/concepts/models/index.md @@ -22,6 +22,8 @@ A sensor model contains the following information: - `decoder`: (optional) instance of a [Decoder] to normalize the data - `metadataMappings`: (optional) metadata mappings (See [Collection Mappings](https://docs.kuzzle.io/core/2/guides/main-concepts/data-storage/#collection-mappings)) - `defaultMetadata`: (optional) default metadata values +- `metadataDetails`: (optional) Metadata group and translations. You can use it to keep consistency on translations between your apps +- `metadataGroups`: (optional) Groups list with translations for group name. You can use it to group metadatas by their concerns It is possible to create new models on the Kuzzle IoT Platform using either: @@ -66,7 +68,9 @@ An asset model contains the following information: - `engineGroup`: engine group to which the model belongs. - `measures`: received measurements - `metadataMappings`: (optional) metadata mappings (See [Collection Mappings](https://docs.kuzzle.io/core/2/guides/main-concepts/data-storage/#collection-mappings)) -- `defaultMetadata`: (optional) default metadata values +- `defaultMetadata`: (optional) default metadata values- +- `metadataDetails`: (optional) Metadata group and translations . You can use it to keep consistency on translations between your apps +- `metadataGroups`: (optional) Groups list with translations for group name. You can use it to group metadatas by their concerns It is possible to create new models on the Kuzzle IoT Platform using either: diff --git a/doc/2/controllers/models/write-asset/index.md b/doc/2/controllers/models/write-asset/index.md index 2172d2ee..5499ab9b 100644 --- a/doc/2/controllers/models/write-asset/index.md +++ b/doc/2/controllers/models/write-asset/index.md @@ -37,7 +37,35 @@ Method: POST }, "defaultValues": { // Default values for metadata - } + }, + "metadataDetails": { + /* + Metadata details including tanslations and group. + [name: string]: { + group?: string; + locales: { + [locale: string]: { + friendlyName: string; + description: string; + }; + }; + }; + */ + }, + "metadataGroups"; { + /* + Metadata groups list and details. + { + [groupName: string]: { + locales: { + [locale: string]: { + groupFriendlyName: string; + }; + }; + }; + }; + */ + }, "measures": [ // Array of measure definition with type and name ] @@ -53,6 +81,8 @@ Method: POST - `model`: Asset model name - `metadataMappings`: Mappings of the metadata in Elasticsearch format - `defaultValues`: Default values for the metadata +- `metadataDetails`: Metadata group and translations +- `metadataGroups`: Groups list with translations for group name - `measures`: Array of measure definition. Each item define a `type` and `name` properties for the measure. --- diff --git a/doc/2/controllers/models/write-device/index.md b/doc/2/controllers/models/write-device/index.md index 3356f47d..72b3051a 100644 --- a/doc/2/controllers/models/write-device/index.md +++ b/doc/2/controllers/models/write-device/index.md @@ -36,7 +36,35 @@ Method: POST }, "defaultValues": { // Default values for metadata - } + }, + "metadataDetails": { + /* + Metadata details including tanslations and group. + [name: string]: { + group?: string; + locales: { + [locale: string]: { + friendlyName: string; + description: string; + }; + }; + }; + */ + }, + "metadataGroups"; { + /* + Metadata groups list and details. + { + [groupName: string]: { + locales: { + [locale: string]: { + groupFriendlyName: string; + }; + }; + }; + }; + */ + }, "measures": [ // Array of measure definition with type and name ] @@ -50,7 +78,9 @@ Method: POST - `model`: Device model name - `metadataMappings`: Mappings of the metadata in Elasticsearch format -- `defaultValues`: Default values for the metadata +- `defaultValues`: Default values for the metadata- +- `metadataDetails`: Metadata group and translations +- `metadataGroups`: Groups list with translations for group name - `measures`: Array of measure definition. Each item define a `type` and `name` properties for the measure. --- diff --git a/lib/modules/model/ModelService.ts b/lib/modules/model/ModelService.ts index 8cf118f3..0d89852d 100644 --- a/lib/modules/model/ModelService.ts +++ b/lib/modules/model/ModelService.ts @@ -15,6 +15,9 @@ import { AssetModelContent, DeviceModelContent, MeasureModelContent, + MetadataDetails, + MetadataGroups, + MetadataMappings, } from "./types/ModelContent"; import { AskModelAssetGet, @@ -59,8 +62,10 @@ export class ModelService extends BaseService { async writeAsset( engineGroup: string, model: string, - metadataMappings: JSONObject, + metadataMappings: MetadataMappings, defaultMetadata: JSONObject, + metadataDetails: MetadataDetails, + metadataGroups: MetadataGroups, measures: AssetModelContent["asset"]["measures"], ): Promise> { if (Inflector.pascalCase(model) !== model) { @@ -68,7 +73,14 @@ export class ModelService extends BaseService { } const modelContent: AssetModelContent = { - asset: { defaultMetadata, measures, metadataMappings, model }, + asset: { + defaultMetadata, + measures, + metadataDetails, + metadataGroups, + metadataMappings, + model, + }, engineGroup, type: "asset", }; @@ -97,7 +109,7 @@ export class ModelService extends BaseService { } private checkDefaultValues( - metadataMappings: JSONObject, + metadataMappings: MetadataMappings, defaultMetadata: JSONObject, ) { const metadata = Object.keys( @@ -121,8 +133,10 @@ export class ModelService extends BaseService { async writeDevice( model: string, - metadataMappings: JSONObject, + metadataMappings: MetadataMappings, defaultMetadata: JSONObject, + metadataDetails: MetadataDetails, + metadataGroups: MetadataGroups, measures: DeviceModelContent["device"]["measures"], ): Promise> { if (Inflector.pascalCase(model) !== model) { @@ -130,7 +144,14 @@ export class ModelService extends BaseService { } const modelContent: DeviceModelContent = { - device: { defaultMetadata, measures, metadataMappings, model }, + device: { + defaultMetadata, + measures, + metadataDetails, + metadataGroups, + metadataMappings, + model, + }, type: "device", }; diff --git a/lib/modules/model/ModelsController.ts b/lib/modules/model/ModelsController.ts index 52bb1e7f..5b495a43 100644 --- a/lib/modules/model/ModelsController.ts +++ b/lib/modules/model/ModelsController.ts @@ -111,12 +111,16 @@ export class ModelsController { const metadataMappings = request.getBodyObject("metadataMappings", {}); const defaultValues = request.getBodyObject("defaultValues", {}); const measures = request.getBodyArray("measures", []); + const metadataDetails = request.getBodyObject("metadataDetails", {}); + const metadataGroups = request.getBodyObject("metadataGroups", {}); const assetModel = await this.modelService.writeAsset( engineGroup, model, metadataMappings, defaultValues, + metadataDetails, + metadataGroups, measures, ); @@ -130,11 +134,15 @@ export class ModelsController { const metadataMappings = request.getBodyObject("metadataMappings", {}); const defaultValues = request.getBodyObject("defaultValues", {}); const measures = request.getBodyArray("measures"); + const metadataDetails = request.getBodyObject("metadataDetails", {}); + const metadataGroups = request.getBodyObject("metadataGroups", {}); const deviceModel = await this.modelService.writeDevice( model, metadataMappings, defaultValues, + metadataDetails, + metadataGroups, measures, ); diff --git a/lib/modules/model/ModelsRegister.ts b/lib/modules/model/ModelsRegister.ts index 897c11aa..d6f12e28 100644 --- a/lib/modules/model/ModelsRegister.ts +++ b/lib/modules/model/ModelsRegister.ts @@ -1,5 +1,4 @@ import { Inflector, PluginContext, PluginImplementationError } from "kuzzle"; -import { JSONObject } from "kuzzle-sdk"; import { DeviceManagerConfiguration, @@ -13,9 +12,13 @@ import { AssetModelContent, DeviceModelContent, MeasureModelContent, + MetadataDetails, + MetadataGroups, + MetadataMappings, ModelContent, } from "./types/ModelContent"; import { ModelSerializer } from "./ModelSerializer"; +import { JSONObject } from "kuzzle-sdk"; export class ModelsRegister { private config: DeviceManagerConfiguration; @@ -46,12 +49,26 @@ export class ModelsRegister { ); } + /** + * Registers an asset model. + * + * @param engineGroup - The engine group name. + * @param model - The name of the asset model, which must be in PascalCase. + * @param measures - The measures associated with this asset model. + * @param metadataMappings - The metadata mappings for the model, defaults to an empty object. + * @param defaultMetadata - The default metadata values for the model, defaults to an empty object. + * @param metadataDetails - Optional detailed metadata descriptions and localizations. + * @param metadataGroups - Optional groups for organizing metadata, with localizations. + * @throws PluginImplementationError if the model name is not in PascalCase. + */ registerAsset( engineGroup: string, model: string, measures: NamedMeasures, - metadataMappings: JSONObject = {}, + metadataMappings: MetadataMappings = {}, defaultMetadata: JSONObject = {}, + metadataDetails: MetadataDetails = {}, + metadataGroups: MetadataGroups = {}, ) { if (Inflector.pascalCase(model) !== model) { throw new PluginImplementationError( @@ -59,18 +76,39 @@ export class ModelsRegister { ); } + // Construct and push the new asset model to the assetModels array this.assetModels.push({ - asset: { defaultMetadata, measures, metadataMappings, model }, + asset: { + defaultMetadata, + measures, + metadataDetails, + metadataGroups, + metadataMappings, + model, + }, engineGroup, type: "asset", }); } + /** + * Registers a device model. + * + * @param model - The name of the device model, which must be in PascalCase. + * @param measures - The measures associated with this device model. + * @param metadataMappings - The metadata mappings for the model, defaults to an empty object. + * @param defaultMetadata - The default metadata values for the model, defaults to an empty object. + * @param metadataDetails - Optional detailed metadata descriptions and localizations. + * @param metadataGroups - Optional groups for organizing metadata, with localizations. + * @throws PluginImplementationError if the model name is not in PascalCase. + */ registerDevice( model: string, measures: NamedMeasures, - metadataMappings: JSONObject = {}, + metadataMappings: MetadataMappings = {}, defaultMetadata: JSONObject = {}, + metadataDetails: MetadataDetails = {}, + metadataGroups: MetadataGroups = {}, ) { if (Inflector.pascalCase(model) !== model) { throw new PluginImplementationError( @@ -78,8 +116,16 @@ export class ModelsRegister { ); } + // Construct and push the new device model to the deviceModels array this.deviceModels.push({ - device: { defaultMetadata, measures, metadataMappings, model }, + device: { + defaultMetadata, + measures, + metadataDetails, + metadataGroups, + metadataMappings, + model, + }, type: "device", }); } diff --git a/lib/modules/model/collections/modelsMappings.ts b/lib/modules/model/collections/modelsMappings.ts index a0842436..955b628f 100644 --- a/lib/modules/model/collections/modelsMappings.ts +++ b/lib/modules/model/collections/modelsMappings.ts @@ -38,6 +38,14 @@ export const modelsMappings: CollectionMappings = { dynamic: "false", properties: {}, }, + metadataDetails: { + dynamic: "false", + properties: {}, + }, + metadataGroups: { + dynamic: "false", + properties: {}, + }, measures: { properties: { type: { type: "keyword" }, @@ -61,6 +69,14 @@ export const modelsMappings: CollectionMappings = { dynamic: "false", properties: {}, }, + metadataDetails: { + dynamic: "false", + properties: {}, + }, + metadataGroups: { + dynamic: "false", + properties: {}, + }, measures: { properties: { type: { type: "keyword" }, diff --git a/lib/modules/model/types/ModelContent.ts b/lib/modules/model/types/ModelContent.ts index fb251a39..9683c886 100644 --- a/lib/modules/model/types/ModelContent.ts +++ b/lib/modules/model/types/ModelContent.ts @@ -11,6 +11,41 @@ export interface MeasureModelContent extends KDocumentContent { type: string; }; } +interface MetadataProperty { + type: string; +} + +export interface MetadataMappings { + [key: string]: + | MetadataProperty + | { properties: { [key: string]: MetadataProperty } }; +} + +interface LocaleDetails { + friendlyName: string; + description: string; +} + +export interface MetadataDetails { + [key: string]: { + group?: string; + locales: { + [locale: string]: LocaleDetails; + }; + }; +} + +interface MetadataGroupLocale { + groupFriendlyName: string; +} + +export interface MetadataGroups { + [groupName: string]: { + locales: { + [locale: string]: MetadataGroupLocale; + }; + }; +} export interface AssetModelContent extends KDocumentContent { type: "asset"; @@ -35,7 +70,7 @@ export interface AssetModelContent extends KDocumentContent { * } * } */ - metadataMappings: JSONObject; + metadataMappings: MetadataMappings; /** * Default values for metadata. @@ -46,7 +81,41 @@ export interface AssetModelContent extends KDocumentContent { * } */ defaultMetadata: JSONObject; - + /** + * Metadata details + * @example + * { + * "extTemp": { + * "group": "buildingEnv", + * "locales": { + * "en": { + * "friendlyName": "External temperature", + * "description": "Building external temperature" + * }, + * "fr": { + * "friendlyName": "Température extérieure", + * "description": "Température à l'exterieur du bâtiment" + * }, + * } + */ + metadataDetails?: MetadataDetails; + /** + * Metadata groups list + * @example + * { + * "buildingEnv": { + * "locales": { + * "en": { + * "groupFriendlyName": "Building environment" + * }, + * "fr": { + * "groupFriendlyName": "Environnement du bâtiment" + * } + * } + * } + * } + */ + metadataGroups?: MetadataGroups; /** * List of accepted measures for this model * @@ -83,8 +152,7 @@ export interface DeviceModelContent extends KDocumentContent { * } * } */ - metadataMappings: JSONObject; - + metadataMappings: MetadataMappings; /** * Default values for metadata. * @@ -94,7 +162,41 @@ export interface DeviceModelContent extends KDocumentContent { * } */ defaultMetadata: JSONObject; - + /** + * Metadata details + * @example + * { + * "sensorVersion": { + * "group": "sensorSpecs", + * "locales": { + * "en": { + * "friendlyName": "Sensor version", + * "description": "Firmware version of the sensor" + * }, + * "fr": { + * "friendlyName": "Version du capteur", + * "description": "Version du micrologiciel du capteur" + * }, + * } + */ + metadataDetails?: MetadataDetails; + /** + * Metadata groups list + * @example + * { + * "sensorSpecs": { + * "locales": { + * "en": { + * "groupFriendlyName": "Sensor specifications" + * }, + * "fr": { + * "groupFriendlyName": "Spécifications techniques" + * } + * } + * } + * } + */ + metadataGroups?: MetadataGroups; /** * List of decoded measures for this model * diff --git a/lib/modules/model/types/ModelDefinition.ts b/lib/modules/model/types/ModelDefinition.ts index bc85121d..a78eb3f3 100644 --- a/lib/modules/model/types/ModelDefinition.ts +++ b/lib/modules/model/types/ModelDefinition.ts @@ -1,6 +1,10 @@ import { JSONObject } from "kuzzle-sdk"; - import { Decoder, NamedMeasures } from "../../../modules/decoder"; +import { + MetadataDetails, + MetadataGroups, + MetadataMappings, +} from "./ModelContent"; /** * Define an asset model @@ -18,6 +22,33 @@ import { Decoder, NamedMeasures } from "../../../modules/decoder"; * }, * defaultMetadata: { * height: 20 + * }, + * metadataDetails: { + * "extTemp": { + * "group": "buildingEnv", + * "locales": { + * "en": { + * "friendlyName": "External temperature", + * "description": "Container external temperature" + * }, + * "fr": { + * "friendlyName": "Température externe", + * "description": "Température externe du conteneur" + * } + * } + * } + * }, + * metadataGroups: { + * "buildingEnv": { + * "locales": { + * "en": { + * "groupFriendlyName": "Building environment" + * }, + * "fr": { + * "groupFriendlyName": "Environnement du bâtiment" + * } + * } + * } * } * } * @@ -31,12 +62,22 @@ export type AssetModelDefinition = { /** * Metadata mappings definition */ - metadataMappings?: JSONObject; + metadataMappings?: MetadataMappings; /** * Default metadata values */ defaultMetadata?: JSONObject; + + /** + * Metadata details including tanslations and group. + */ + metadataDetails?: MetadataDetails; + + /** + * Metadata groups + */ + metadataGroups?: MetadataGroups; }; /** @@ -47,6 +88,36 @@ export type AssetModelDefinition = { * decoder: new DummyTempPositionDecoder(), * metadataMappings: { * serial: { type: "keyword" }, + * }, + * defaultMetadata: { + * company: "Firebird" + * }, + * metadataDetails: { + * sensorType: { + * group: "sensorSpecs", + * locales: { + * en: { + * friendlyName: "Sensor type", + * description: "Type of the sensor" + * }, + * fr: { + * friendlyName: "Type de traceur", + * description: "Type du traceur" + * } + * } + * } + * }, + * metadataGroups: { + * sensorSpecs: { + * locales: { + * en: { + * groupFriendlyName: "Sensors specifications" + * }, + * fr: { + * groupFriendlyName: "Spécifications des capteurs" + * } + * } + * } * } * } * @@ -56,12 +127,24 @@ export type DeviceModelDefinition = { * Decoder used to decode payloads */ decoder: Decoder; + /** * Metadata mappings definition */ - metadataMappings?: JSONObject; + metadataMappings?: MetadataMappings; + /** * Default metadata values */ defaultMetadata?: JSONObject; + + /** + * Metadata details including tanslations and group. + */ + metadataDetails?: MetadataDetails; + + /** + * Metadata groups list and details. + */ + metadataGroups?: MetadataGroups; }; diff --git a/lib/modules/plugin/DeviceManagerPlugin.ts b/lib/modules/plugin/DeviceManagerPlugin.ts index c7a98c74..21ebf9ab 100644 --- a/lib/modules/plugin/DeviceManagerPlugin.ts +++ b/lib/modules/plugin/DeviceManagerPlugin.ts @@ -67,10 +67,13 @@ export class DeviceManagerPlugin extends Plugin { * Register an asset model * * @param engineGroup Engine group name - * @param model Name of the asset model - * @param definition.measures Array describing measures names and types - * @param definition.metadataMappings Metadata mappings definition - * @param definition.defaultMetadata Default metadata values + * @param model Name of the asset model. Must follow a naming convention in PascalCase. + * @param definition Object containing the asset model definition, including: + * - measures: Array describing measure names and their types. + * - metadataMappings: Definition of metadata mappings, specifying types for each metadata field. + * - defaultMetadata: Default values for metadata fields, applied when actual data is not provided. + * - metadataDetails: Optional detailed descriptions for each metadata, including group association and localizations. + * - metadataGroups: Optional description of metadata groups, organizing metadata logically, with localizations for group names. * * @example * ``` @@ -89,6 +92,33 @@ export class DeviceManagerPlugin extends Plugin { * }, * defaultMetadata: { * height: 20 + * }, + * metadataDetails: { + * "extTemp": { + * "group": "environment", + * "locales": { + * "en": { + * "friendlyName": "External Temperature", + * "description": "The temperature outside the container" + * }, + * "fr": { + * "friendlyName": "Température Externe", + * "description": "La température à l'extérieur du conteneur" + * } + * } + * } + * }, + * metadataGroups: { + * "environment": { + * "locales": { + * "en": { + * "groupFriendlyName": "Environment" + * }, + * "fr": { + * "groupFriendlyName": "Environnement" + * } + * } + * } * } * } * ); @@ -105,16 +135,21 @@ export class DeviceManagerPlugin extends Plugin { definition.measures, definition.metadataMappings, definition.defaultMetadata, + definition.metadataDetails, + definition.metadataGroups, ); }, /** - * Register a device model + * Register a device. * * @param model Name of the device model - * @param definition.decoded Decoder used to decode payloads - * @param definition.metadataMappings Metadata mappings definition - * @param definition.defaultMetadata Default metadata values + * @param definition Object containing the device model definition, including: + * - decoder: Decoder used to decode payloads + * - metadataMappings: Metadata mappings definition + * - defaultMetadata: Default metadata values + * - metadataDetails: Detailed metadata descriptions and localizations + * - metadataGroups: Groups for organizing metadata, with localizations * * @example * ``` @@ -124,6 +159,36 @@ export class DeviceManagerPlugin extends Plugin { * decoder: new DummyTempPositionDecoder(), * metadataMappings: { * serial: { type: "keyword" }, + * }, + * defaultMetadata: { + * company: "Acme Inc" + * }, + * metadataDetails: { + * sensorVersion: { + * group: "sensorSpecs", + * locales: { + * en: { + * friendlyName: "Sensor version", + * description: "Firmware version of the sensor" + * }, + * fr: { + * friendlyName: "Version du capteur", + * description: "Version du micrologiciel du capteur" + * } + * } + * } + * }, + * metadataGroups: { + * sensorSpecs: { + * locales: { + * en: { + * groupFriendlyName: "Sensors specifications" + * }, + * fr: { + * groupFriendlyName: "Spécifications des capteurs" + * } + * } + * } * } * } * ); @@ -137,6 +202,8 @@ export class DeviceManagerPlugin extends Plugin { definition.decoder.measures as NamedMeasures, definition.metadataMappings, definition.defaultMetadata, + definition.metadataDetails, + definition.metadataGroups, ); }, diff --git a/tests/application/assets/Container.ts b/tests/application/assets/Container.ts index 73c3a8b6..75f32c16 100644 --- a/tests/application/assets/Container.ts +++ b/tests/application/assets/Container.ts @@ -9,7 +9,6 @@ import { export interface ContainerMetadata extends Metadata { height: number; width: number; - trailer: { weight: number; capacity: number; @@ -23,8 +22,7 @@ export type ContainerMeasurements = { temperatureWeather: TemperatureMeasurement; }; -export interface ContainerAssetContent - extends AssetContent { +export interface ContainerAssetContent extends AssetContent { model: "Container"; } @@ -48,12 +46,94 @@ export const containerAssetDefinition: AssetModelDefinition = { defaultMetadata: { height: 20, }, + metadataDetails: { + extTemp: { + group: "environment", + locales: { + en: { + friendlyName: "External Temperature", + description: "The temperature outside the container" + }, + fr: { + friendlyName: "Température Externe", + description: "La température à l'extérieur du conteneur" + } + } + }, + intTemp: { + group: "environment", + locales: { + en: { + friendlyName: "Internal Temperature", + description: "The temperature inside the container" + }, + fr: { + friendlyName: "Température Interne", + description: "La température à l'intérieur du conteneur" + } + } + } + }, + metadataGroups: { + environment: { + locales: { + en: { + groupFriendlyName: "Environmental Measurements" + }, + fr: { + groupFriendlyName: "Mesures environnementales" + } + } + }, + }, +}; + + +// Mocked data example to match the expected type structure +const temperatureMeasureExample = { + payloadUuids: ["uuid1", "uuid2"], + type: "temperature", + measuredAt: new Date().getTime(), + name: "temperatureExt", + originId: "someOriginId", + values: { + temperature: 20, + }, +}; + +const positionMeasureExample = { + payloadUuids: ["uuid3", "uuid4"], + type: "position", + measuredAt: new Date().getTime(), + name: "position", + originId: "someOriginId", + values: { + position: { + lat: 0, + lon: 0, + }, + accuracy: 10, + }, +}; + +const measures = { + temperatureExt: temperatureMeasureExample, + temperatureInt: { ...temperatureMeasureExample, name: "temperatureInt", values: { temperature: 22 } }, + position: positionMeasureExample, + temperatureWeather: { ...temperatureMeasureExample, name: "temperatureWeather", values: { temperature: 15 } }, }; // This function is never called and only exists to make sure the types are correct function neverCalled() { - // @ts-ignore - const container: ContainerAssetContent = {}; + const container: ContainerAssetContent = { + model: "Container", + linkedDevices: [], + groups: [], + reference: "", + metadata: undefined, + measures, + lastMeasuredAt: 0 + }; container.metadata.height = 40; if (container.measures.temperatureExt) { diff --git a/tests/hooks/collections.ts b/tests/hooks/collections.ts index 1aa8b6d8..cf8490f8 100644 --- a/tests/hooks/collections.ts +++ b/tests/hooks/collections.ts @@ -17,6 +17,7 @@ async function deleteModels(sdk: Kuzzle) { values: [ "model-measure-presence", "model-asset-plane", + "model-asset-AdvancedPlane", "model-device-Zigbee", "model-device-Enginko", ], diff --git a/tests/scenario/migrated/model-controller.test.ts b/tests/scenario/migrated/model-controller.test.ts index 482a9bb9..b3e23b80 100644 --- a/tests/scenario/migrated/model-controller.test.ts +++ b/tests/scenario/migrated/model-controller.test.ts @@ -497,7 +497,61 @@ describe("features/Model/Controller", () => { _source: { measure: { type: "battery" } }, }); }); + it("Write and Retrieve an Asset model with metadata details and groups", async () => { + const assetModelWithDetailsAndGroups = { + engineGroup: "commons", + model: "AdvancedPlane", + metadataMappings: { + company: { type: "keyword" }, + year: { type: "integer" } + }, + measures: [ + { name: "temperatureExt", type: "temperature" }, + ], + metadataDetails: { + company: { + group: "companyInfo", + locales: { + en: { + friendlyName: "Manufacturer", + description: "The company that manufactured the plane" + }, + fr: { + friendlyName: "Fabricant", + description: "L'entreprise qui a fabriqué l'avion" + } + } + } + }, + metadataGroups: { + companyInfo: { + locales: { + en: { groupFriendlyName: "Company Information" }, + fr: { groupFriendlyName: "Informations sur l'entreprise" } + } + } + } + }; + + // Write the asset model with metadata details and groups + await sdk.query({ + controller: "device-manager/models", + action: "writeAsset", + body: assetModelWithDetailsAndGroups + }); + // Retrieve and assert the asset model + const response = await sdk.document.get("device-manager", "models", "model-asset-AdvancedPlane"); + expect(response._source.asset).toHaveProperty('metadataDetails'); + expect(response._source.asset).toHaveProperty('metadataGroups'); + delete assetModelWithDetailsAndGroups.engineGroup + expect(response._source).toMatchObject({ + type: "asset", + engineGroup: "commons", + asset: assetModelWithDetailsAndGroups + }); + }); + it("Register models from the framework", async () => { let response; let promise;