diff --git a/doc/2/controllers/assets/upsert/index.md b/doc/2/controllers/assets/upsert/index.md new file mode 100644 index 00000000..dfead7f7 --- /dev/null +++ b/doc/2/controllers/assets/upsert/index.md @@ -0,0 +1,67 @@ +--- +code: true +type: page +title: upsert +description: Update or Create an asset +--- + +# upsert + +Update or Create an asset. +The Upsert operation allows you to create a new asset or update an existing one if it already exists. This operation is useful when you want to ensure that an asset is either created or updated in a single request. + +## Query Syntax + +### HTTP + +```http +URL: http://kuzzle:7512/_/device-manager/:engineId/assets/:_id +Method: POST +``` + +## Other protocols + +```js +{ + "controller": "device-manager/assets", + "action": "upsert", + "engineId": "", + "_id": "", + "body": { + "metadata": { + "": "" + } + } +} +``` + +--- + +## Arguments + +- `engineId`: Engine ID +- `_id`: Asset ID + +## Body properties + +- `metadata`: Object containing metadata + +--- + +## Response + +```js +{ + "status": 200, + "error": null, + "controller": "device-manager/assets", + "action": "update", + "requestId": "", + "result": { + "_id": "", + "_source": { + // Asset content + }, + } +} +``` diff --git a/lib/modules/asset/AssetService.ts b/lib/modules/asset/AssetService.ts index 222915dd..6c2fc2fb 100644 --- a/lib/modules/asset/AssetService.ts +++ b/lib/modules/asset/AssetService.ts @@ -80,7 +80,7 @@ export class AssetService extends BaseService { } /** - * Updates an asset metadata + * Update an asset metadata */ public async update( engineId: string, @@ -135,6 +135,73 @@ export class AssetService extends BaseService { }); } + /** + * Update or Create an asset metadata + */ + public async upsert( + engineId: string, + assetId: string, + model: string, + reference: string, + metadata: Metadata, + request: KuzzleRequest + ): Promise> { + return lock(`asset:${engineId}:${assetId}`, async () => { + const asset = await this.get(engineId, assetId, request).catch( + () => null + ); + + if (!asset) { + return this.create(engineId, model, reference, metadata, request); + } + + const updatedPayload = await this.app.trigger( + "device-manager:asset:update:before", + { asset, metadata } + ); + + const updatedAsset = await this.updateDocument( + request, + { + _id: assetId, + _source: { metadata: updatedPayload.metadata }, + }, + { + collection: InternalCollection.ASSETS, + engineId, + }, + { source: true } + ); + + await this.assetHistoryService.add(engineId, [ + { + asset: updatedAsset._source, + event: { + metadata: { + names: Object.keys(flattenObject(updatedPayload.metadata)), + }, + name: "metadata", + }, + id: updatedAsset._id, + timestamp: Date.now(), + }, + ]); + + await this.app.trigger( + "device-manager:asset:update:after", + { + asset: updatedAsset, + metadata: updatedPayload.metadata, + } + ); + + return updatedAsset; + }); + } + + /** + * Create an asset metadata + */ public async create( engineId: string, model: string, @@ -207,6 +274,9 @@ export class AssetService extends BaseService { }); } + /** + * Delete an asset metadata + */ public async delete( engineId: string, assetId: string, diff --git a/lib/modules/asset/AssetsController.ts b/lib/modules/asset/AssetsController.ts index 7801420e..dd59bc34 100644 --- a/lib/modules/asset/AssetsController.ts +++ b/lib/modules/asset/AssetsController.ts @@ -13,6 +13,7 @@ import { AssetService } from "./AssetService"; import { AssetSerializer } from "./model/AssetSerializer"; import { ApiAssetCreateResult, + ApiAssetUpsertResult, ApiAssetDeleteResult, ApiAssetGetMeasuresResult, ApiAssetGetResult, @@ -37,6 +38,12 @@ export class AssetsController { handler: this.create.bind(this), http: [{ path: "device-manager/:engineId/assets", verb: "post" }], }, + upsert: { + handler: this.upsert.bind(this), + http: [ + { path: "device-manager/:engineId/assets/:_id", verb: "post" }, + ], + }, delete: { handler: this.delete.bind(this), http: [ @@ -129,6 +136,25 @@ export class AssetsController { return AssetSerializer.serialize(asset); } + async upsert(request: KuzzleRequest): Promise { + const engineId = request.getString("engineId"); + const assetId = request.getId(); + const model = request.getBodyString("model"); + const reference = request.getBodyString("reference"); + const metadata = request.getBodyObject("metadata"); + + const upsertAsset = await this.assetService.upsert( + engineId, + assetId, + model, + reference, + metadata, + request + ); + + return AssetSerializer.serialize(upsertAsset); + } + async update(request: KuzzleRequest): Promise { const assetId = request.getId(); const engineId = request.getString("engineId"); diff --git a/lib/modules/asset/types/AssetApi.ts b/lib/modules/asset/types/AssetApi.ts index c93ff497..24d4acf3 100644 --- a/lib/modules/asset/types/AssetApi.ts +++ b/lib/modules/asset/types/AssetApi.ts @@ -31,6 +31,23 @@ export interface ApiAssetUpdateRequest extends AssetsControllerRequest { } export type ApiAssetUpdateResult = KDocument; +export interface ApiAssetUpsertRequest extends AssetsControllerRequest { + action: "upsert"; + + _id: string; + + refresh?: string; + + body: { + model: string; + + reference: string; + + metadata: Metadata; + }; +} +export type ApiAssetUpsertResult = KDocument; + export interface ApiAssetCreateRequest extends AssetsControllerRequest { action: "create"; diff --git a/lib/modules/shared/services/BaseService.ts b/lib/modules/shared/services/BaseService.ts index dd7aa535..742b0f5f 100644 --- a/lib/modules/shared/services/BaseService.ts +++ b/lib/modules/shared/services/BaseService.ts @@ -2,6 +2,7 @@ import { ArgsDocumentControllerCreate, ArgsDocumentControllerDelete, ArgsDocumentControllerUpdate, + ArgsDocumentControllerUpsert, Backend, BaseRequest, DocumentSearchResult, diff --git a/tests/scenario/migrated/asset-controller.test.ts b/tests/scenario/migrated/asset-controller.test.ts index e3dc854a..34efafe9 100644 --- a/tests/scenario/migrated/asset-controller.test.ts +++ b/tests/scenario/migrated/asset-controller.test.ts @@ -172,4 +172,68 @@ describe("features/Asset/Controller", () => { sdk.document.exists("engine-kuzzle", "devices", "DummyTemp-foobar") ).resolves.toBe(true); }); + + it("Upsert asset", async () => { + const response = await sdk.query({ + controller: "device-manager/assets", + action: "upsert", + engineId: "engine-kuzzle", + _id: "Container-linked2", + body: { + model: "Container", + reference: "linked2", + metadata: { height: 21, weight: 42 }, + }, + }); + + expect(response).toBeDefined(); + expect(response.result).toBeDefined(); + expect(response.result._id).toEqual("Container-linked2"); + expect(response.result._source.model).toEqual("Container"); + expect(response.result._source.reference).toEqual("linked2"); + expect(response.result._source.metadata).toEqual({ + height: 21, + trailer: null, + weight: 42, + }); + }); + + it("Upsert asset - update existing asset", async () => { + // create asset + await sdk.query({ + controller: "device-manager/assets", + action: "upsert", + engineId: "engine-kuzzle", + _id: "Container-linked2", + body: { + model: "Container", + reference: "linked2", + metadata: { height: 21, trailer: null, weight: 42 }, + }, + }); + + // update asset + const response = await sdk.query({ + controller: "device-manager/assets", + action: "upsert", + engineId: "engine-kuzzle", + _id: "Container-linked2", + body: { + model: "Container", + reference: "linked2", + metadata: { height: 22, trailer: null, weight: 43 }, + }, + }); + + expect(response).toBeDefined(); + expect(response.result).toBeDefined(); + expect(response.result._id).toEqual("Container-linked2"); + expect(response.result._source.model).toEqual("Container"); + expect(response.result._source.reference).toEqual("linked2"); + expect(response.result._source.metadata).toEqual({ + height: 22, + trailer: null, + weight: 43, + }); + }); });