diff --git a/doc/2/controllers/models/search-assets/index.md b/doc/2/controllers/models/search-assets/index.md new file mode 100644 index 00000000..055aa2a8 --- /dev/null +++ b/doc/2/controllers/models/search-assets/index.md @@ -0,0 +1,81 @@ +--- +code: true +type: page +title: searchAssets +description: Searches for asset models +--- + +# searchAssets + +Searches for asset models. + +--- + +## Query Syntax + +### HTTP + +```http +URL: http://kuzzle:7512/_/device-manager/models/assets/_search +Method: POST +``` + +### Other protocols + +```js +{ + "controller": "device-manager/models", + "action": "searchAssets", + "engineGroup": "", + "body": { + "query": { + // ... + }, + "sort": [ + // ... + ] + }, + + // optional: + "from": "", + "size": "" +} +``` + +--- + +## Arguments + +- `engineGroup`: name of the engine group +- `from`: paginates search results by defining the offset from the first result you want to fetch. Usually used with the `size` argument +- `size`: set the maximum number of documents returned per result page + +## Body properties + +- `query`: the search query itself, using the [ElasticSearch Query DSL](https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl.html). +- `sort`: contains a list of fields, used to [sort search results](https://www.elastic.co/guide/en/elasticsearch/reference/7.4/search-request-sort.html), in order of importance. + +--- + +## Response + +```js +{ + "status": 200, + "error": null, + "controller": "device-manager/models", + "action": "searchAssets", + "requestId": "", + "result": { + "hits": [ + { + "_id": "", + "_source": { + // Asset model content + }, + }, + ], + "total": 42 + } +} +``` diff --git a/doc/2/controllers/models/search-devices/index.md b/doc/2/controllers/models/search-devices/index.md new file mode 100644 index 00000000..9724d96c --- /dev/null +++ b/doc/2/controllers/models/search-devices/index.md @@ -0,0 +1,79 @@ +--- +code: true +type: page +title: searchDevices +description: Searches for device models +--- + +# searchDevices + +Searches for device models. + +--- + +## Query Syntax + +### HTTP + +```http +URL: http://kuzzle:7512/_/device-manager/models/devices/_search +Method: POST +``` + +### Other protocols + +```js +{ + "controller": "device-manager/models", + "action": "searchDevices", + "body": { + "query": { + // ... + }, + "sort": [ + // ... + ] + }, + + // optional: + "from": "", + "size": "" +} +``` + +--- + +## Arguments + +- `from`: paginates search results by defining the offset from the first result you want to fetch. Usually used with the `size` argument +- `size`: set the maximum number of documents returned per result page + +## Body properties + +- `query`: the search query itself, using the [ElasticSearch Query DSL](https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl.html). +- `sort`: contains a list of fields, used to [sort search results](https://www.elastic.co/guide/en/elasticsearch/reference/7.4/search-request-sort.html), in order of importance. + +--- + +## Response + +```js +{ + "status": 200, + "error": null, + "controller": "device-manager/models", + "action": "searchDevices", + "requestId": "", + "result": { + "hits": [ + { + "_id": "", + "_source": { + // Device model content + }, + }, + ], + "total": 42 + } +} +``` diff --git a/doc/2/controllers/models/search-measures/index.md b/doc/2/controllers/models/search-measures/index.md new file mode 100644 index 00000000..7f05a25e --- /dev/null +++ b/doc/2/controllers/models/search-measures/index.md @@ -0,0 +1,79 @@ +--- +code: true +type: page +title: searchMeasures +description: Searches for measure models +--- + +# searchMeasures + +Searches for measure models. + +--- + +## Query Syntax + +### HTTP + +```http +URL: http://kuzzle:7512/_/device-manager/models/measures/_search +Method: POST +``` + +### Other protocols + +```js +{ + "controller": "device-manager/models", + "action": "searchMeasures", + "body": { + "query": { + // ... + }, + "sort": [ + // ... + ] + }, + + // optional: + "from": "", + "size": "" +} +``` + +--- + +## Arguments + +- `from`: paginates search results by defining the offset from the first result you want to fetch. Usually used with the `size` argument +- `size`: set the maximum number of documents returned per result page + +## Body properties + +- `query`: the search query itself, using the [ElasticSearch Query DSL](https://www.elastic.co/guide/en/elasticsearch/reference/7.4/query-dsl.html). +- `sort`: contains a list of fields, used to [sort search results](https://www.elastic.co/guide/en/elasticsearch/reference/7.4/search-request-sort.html), in order of importance. + +--- + +## Response + +```js +{ + "status": 200, + "error": null, + "controller": "device-manager/models", + "action": "searchMeasures", + "requestId": "", + "result": { + "hits": [ + { + "_id": "", + "_source": { + // Measure model content + }, + }, + ], + "total": 42 + } +} +``` diff --git a/lib/modules/model/ModelService.ts b/lib/modules/model/ModelService.ts index e548ac68..6d58f050 100644 --- a/lib/modules/model/ModelService.ts +++ b/lib/modules/model/ModelService.ts @@ -9,7 +9,7 @@ import { NotFoundError, } from "kuzzle"; import { ask, onAsk } from "kuzzle-plugin-commons"; -import { JSONObject, KDocument } from "kuzzle-sdk"; +import { JSONObject, KDocument, KHit, SearchResult } from "kuzzle-sdk"; import { AskEngineUpdateAll, @@ -19,7 +19,7 @@ import { } from "../plugin"; import { AskAssetRefreshModel } from "../asset"; -import { BaseService, flattenObject } from "../shared"; +import { BaseService, SearchParams, flattenObject } from "../shared"; import { ModelSerializer } from "./ModelSerializer"; import { AssetModelContent, @@ -387,60 +387,116 @@ export class ModelService extends BaseService { async listAsset( engineGroup: string, ): Promise[]> { + const result = await this.searchAssets(engineGroup, { + searchBody: { + sort: { "asset.model": "asc" }, + }, + size: 5000, + }); + + return result.hits; + } + + async listDevices(): Promise[]> { + const result = await this.searchDevices({ + searchBody: { + sort: { "device.model": "asc" }, + }, + size: 5000, + }); + + return result.hits; + } + + async listMeasures(): Promise[]> { + const result = await this.searchMeasures({ + searchBody: { + sort: { "measure.type": "asc" }, + }, + size: 5000, + }); + + return result.hits; + } + + async searchAssets( + engineGroup: string, + searchParams: Partial, + ): Promise>> { const query = { - and: [ - { equals: { type: "asset" } }, - { - or: [ - { equals: { engineGroup } }, - { equals: { engineGroup: "commons" } }, - ], - }, - ], + bool: { + must: [searchParams.searchBody.query, { match: { type: "asset" } }], + should: [ + { match: { engineGroup } }, + { match: { engineGroup: "commons" } }, + ], + }, }; - const sort = { "asset.model": "asc" }; - - const result = await this.sdk.document.search( + return this.sdk.document.search( this.config.adminIndex, InternalCollection.MODELS, - { query, sort }, - { lang: "koncorde", size: 5000 }, + { + ...searchParams.searchBody, + query, + }, + { + from: searchParams.from, + lang: "elasticsearch", + scroll: searchParams.scrollTTL, + size: searchParams.size, + }, ); - - return result.hits; } - async listDevices(): Promise[]> { + async searchDevices( + searchParams: Partial, + ): Promise>> { const query = { - and: [{ equals: { type: "device" } }], + bool: { + must: [searchParams.searchBody.query, { match: { type: "device" } }], + }, }; - const sort = { "device.model": "asc" }; - const result = await this.sdk.document.search( + return this.sdk.document.search( this.config.adminIndex, InternalCollection.MODELS, - { query, sort }, - { lang: "koncorde", size: 5000 }, + { + ...searchParams.searchBody, + query, + }, + { + from: searchParams.from, + lang: "elasticsearch", + scroll: searchParams.scrollTTL, + size: searchParams.size, + }, ); - - return result.hits; } - async listMeasures(): Promise[]> { + async searchMeasures( + searchParams: Partial, + ): Promise>> { const query = { - and: [{ equals: { type: "measure" } }], + bool: { + must: [searchParams.searchBody.query, { match: { type: "measure" } }], + }, }; - const sort = { "measure.type": "asc" }; - const result = await this.sdk.document.search( + return this.sdk.document.search( this.config.adminIndex, InternalCollection.MODELS, - { query, sort }, - { lang: "koncorde", size: 5000 }, + { + ...searchParams.searchBody, + query, + }, + { + from: searchParams.from, + lang: "elasticsearch", + scroll: searchParams.scrollTTL, + size: searchParams.size, + }, ); - - return result.hits; } async assetExists(model: string): Promise { diff --git a/lib/modules/model/ModelsController.ts b/lib/modules/model/ModelsController.ts index 46411fda..f462e098 100644 --- a/lib/modules/model/ModelsController.ts +++ b/lib/modules/model/ModelsController.ts @@ -15,6 +15,9 @@ import { ApiModelGetAssetResult, ApiModelGetDeviceResult, ApiModelGetMeasureResult, + ApiModelSearchAssetsResult, + ApiModelSearchDevicesResult, + ApiModelSearchMeasuresResult, } from "./types/ModelApi"; export class ModelsController { @@ -65,6 +68,24 @@ export class ModelsController { handler: this.listMeasures.bind(this), http: [{ path: "device-manager/models/measures", verb: "get" }], }, + searchAssets: { + handler: this.searchAssets.bind(this), + http: [ + { path: "device-manager/models/assets/_search", verb: "post" }, + ], + }, + searchDevices: { + handler: this.searchDevices.bind(this), + http: [ + { path: "device-manager/models/devices/_search", verb: "post" }, + ], + }, + searchMeasures: { + handler: this.searchMeasures.bind(this), + http: [ + { path: "device-manager/models/measures/_search", verb: "post" }, + ], + }, updateAsset: { handler: this.updateAsset.bind(this), http: [ @@ -226,6 +247,27 @@ export class ModelsController { }; } + async searchAssets( + request: KuzzleRequest, + ): Promise { + return this.modelService.searchAssets( + request.getString("engineGroup"), + request.getSearchParams(), + ); + } + + async searchDevices( + request: KuzzleRequest, + ): Promise { + return this.modelService.searchDevices(request.getSearchParams()); + } + + async searchMeasures( + request: KuzzleRequest, + ): Promise { + return this.modelService.searchMeasures(request.getSearchParams()); + } + async updateAsset( request: KuzzleRequest, ): Promise { diff --git a/lib/modules/model/types/ModelApi.ts b/lib/modules/model/types/ModelApi.ts index c96f47b8..9408a053 100644 --- a/lib/modules/model/types/ModelApi.ts +++ b/lib/modules/model/types/ModelApi.ts @@ -1,4 +1,4 @@ -import { JSONObject, KDocument } from "kuzzle-sdk"; +import { JSONObject, KDocument, KHit, SearchResult } from "kuzzle-sdk"; import { AssetModelContent, @@ -138,3 +138,24 @@ export type ApiModelListMeasuresResult = { models: KDocument[]; total: number; }; + +export interface ApiModelSearchAssetsRequest extends ModelsControllerRequest { + action: "searchAssets"; + + engineGroup: string; +} +export type ApiModelSearchAssetsResult = SearchResult>; + +export interface ApiModelSearchDevicesRequest extends ModelsControllerRequest { + action: "searchDevices"; +} +export type ApiModelSearchDevicesResult = SearchResult< + KHit +>; + +export interface ApiModelSearchMeasuresRequest extends ModelsControllerRequest { + action: "searchMeasures"; +} +export type ApiModelSearchMeasuresResult = SearchResult< + KHit +>; diff --git a/tests/hooks/collections.ts b/tests/hooks/collections.ts index d3911e27..e33bc01c 100644 --- a/tests/hooks/collections.ts +++ b/tests/hooks/collections.ts @@ -19,6 +19,7 @@ async function deleteModels(sdk: Kuzzle) { "model-asset-plane", "model-asset-AdvancedPlane", "model-device-Zigbee", + "model-device-Bluetooth", "model-device-Enginko", "model-asset-TestHouse", "model-asset-AdvancedWarehouse", diff --git a/tests/scenario/modules/models/asset-model.test.ts b/tests/scenario/modules/models/asset-model.test.ts index 9dfd96ed..a913e62f 100644 --- a/tests/scenario/modules/models/asset-model.test.ts +++ b/tests/scenario/modules/models/asset-model.test.ts @@ -107,6 +107,56 @@ describe("ModelsController:assets", () => { await expect(getAssetNotExist).rejects.toMatchObject({ status: 404 }); }); + it("Write and Search an Asset model", async () => { + await sdk.query({ + controller: "device-manager/models", + action: "writeAsset", + body: { + engineGroup: "commons", + model: "Plane", + metadataMappings: { company: { type: "keyword" } }, + measures: [{ name: "temperatureExt", type: "temperature" }], + }, + }); + + await sdk.query({ + controller: "device-manager/models", + action: "writeAsset", + body: { + engineGroup: "commons", + model: "Car", + metadataMappings: { + company: { type: "keyword" }, + company2: { type: "keyword" }, + }, + measures: [ + { name: "temperatureExt", type: "temperature" }, + { name: "position", type: "position" }, + ], + }, + }); + + await sdk.collection.refresh("device-manager", "models"); + + const searchAssets = await sdk.query({ + controller: "device-manager/models", + action: "searchAssets", + engineGroup: "commons", + body: { + query: { + match: { + "asset.model": "Plane", + }, + }, + }, + }); + + expect(searchAssets.result).toMatchObject({ + total: 1, + hits: [{ _id: "model-asset-Plane" }], + }); + }); + it("Error if the model name is not PascalCase", async () => { const badModelName = sdk.query({ controller: "device-manager/models", diff --git a/tests/scenario/modules/models/device-model.test.ts b/tests/scenario/modules/models/device-model.test.ts index 350247f1..e4751706 100644 --- a/tests/scenario/modules/models/device-model.test.ts +++ b/tests/scenario/modules/models/device-model.test.ts @@ -121,6 +121,53 @@ describe("ModelsController:devices", () => { }); }); + it("Write and Search a Device model", async () => { + await sdk.query({ + controller: "device-manager/models", + action: "writeDevice", + body: { + model: "Zigbee", + measures: [{ type: "battery", name: "battery" }], + metadataMappings: { network: { type: "keyword" } }, + }, + }); + + await sdk.query({ + controller: "device-manager/models", + action: "writeDevice", + body: { + model: "Bluetooth", + measures: [ + { type: "battery", name: "battery" }, + { type: "temperature", name: "temperature" }, + ], + metadataMappings: { + network: { type: "keyword" }, + network2: { type: "keyword" }, + }, + }, + }); + + await sdk.collection.refresh("device-manager", "models"); + + const searchDevices = await sdk.query({ + controller: "device-manager/models", + action: "searchDevices", + body: { + query: { + match: { + "device.model": "Zigbee", + }, + }, + }, + }); + + expect(searchDevices.result).toMatchObject({ + total: 1, + hits: [{ _id: "model-device-Zigbee" }], + }); + }); + it("Error if the model name is not PascalCase", async () => { const badModelName = sdk.query({ controller: "device-manager/models", diff --git a/tests/scenario/modules/models/measures-model.test.ts b/tests/scenario/modules/models/measures-model.test.ts index b902ad1c..84fea05d 100644 --- a/tests/scenario/modules/models/measures-model.test.ts +++ b/tests/scenario/modules/models/measures-model.test.ts @@ -11,7 +11,7 @@ import { } from "../../../../lib/modules/model"; import { setupSdK } from "../../../helpers"; -jest.setTimeout(10000); +jest.setTimeout(20000); describe("ModelsController:measures", () => { const sdk = setupSdK(); @@ -184,6 +184,47 @@ describe("ModelsController:measures", () => { }); }); + it("Write and Search a Measure model", async () => { + await sdk.query({ + controller: "device-manager/models", + action: "writeMeasure", + body: { + type: "presence", + valuesMappings: { presence: { type: "boolean" } }, + }, + }); + + await sdk.query({ + controller: "device-manager/models", + action: "writeMeasure", + body: { + type: "movement", + valuesMappings: { + movement: { type: "boolean" }, + }, + }, + }); + + await sdk.collection.refresh("device-manager", "models"); + + const searchMeasures = await sdk.query({ + controller: "device-manager/models", + action: "searchMeasures", + body: { + query: { + match: { + "measure.type": "presence", + }, + }, + }, + }); + + expect(searchMeasures.result).toMatchObject({ + total: 1, + hits: [{ _id: "model-measure-presence" }], + }); + }); + it("Register models from the framework", async () => { const temperatureModel = await sdk.document.get( "device-manager",