Skip to content

Commit

Permalink
feat: upsert devices (#355)
Browse files Browse the repository at this point in the history
* feat: upsert devices

* fixup! feat: upsert devices

* fixup! feat: upsert devices
  • Loading branch information
Juiced66 authored Jul 19, 2024
1 parent 6d22fa9 commit c75abef
Show file tree
Hide file tree
Showing 6 changed files with 312 additions and 2 deletions.
6 changes: 4 additions & 2 deletions doc/2/controllers/assets/upsert/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ Method: POST
"controller": "device-manager/assets",
"action": "upsert",
"engineId": "<engineId>",
"_id": "<assetId>",
"reference": "<assetReference>",
"model": "<assetModel>",
"body": {
"metadata": {
"<metadata name>": "<metadata value>"
Expand All @@ -40,7 +41,8 @@ Method: POST
## Arguments

- `engineId`: Engine ID
- `_id`: Asset ID
- `reference` : asset reference
- `model`: asset model

## Body properties

Expand Down
69 changes: 69 additions & 0 deletions doc/2/controllers/devices/upsert/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
code: true
type: page
title: upsert
description: Update or Create a device
---

# upsert

Update or Create a device.
The Upsert operation allows you to create a new device or update an existing one if it already exists. This operation is useful when you want to ensure that an device is either created or updated in a single request.

## Query Syntax

### HTTP

```http
URL: http://kuzzle:7512/_/device-manager/:engineId/devices/:_id
Method: POST
```

## Other protocols

```js
{
"controller": "device-manager/devices",
"action": "upsert",
"engineId": "<engineId>",
"reference": "<deviceReference>",
"model": "<deviceModel>",
"body": {
"metadata": {
"<metadata name>": "<metadata value>"
}
}
}
```

---

## Arguments

- `engineId`: Engine ID
- `reference` : device reference
- `model`: device model

## Body properties

- `metadata`: Object containing metadata

---

## Response

```js
{
"status": 200,
"error": null,
"controller": "device-manager/devices",
"action": "update",
"requestId": "<unique request identifier>",
"result": {
"_id": "<deviceId>",
"_source": {
// device content
},
}
}
```
69 changes: 69 additions & 0 deletions lib/modules/device/DeviceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,75 @@ export class DeviceService extends BaseService {
);
}

/**
* Update or Create an device metadata
*/
public async upsert(
engineId: string,
model: string,
reference: string,
metadata: Metadata,
request: KuzzleRequest,
): Promise<KDocument<DeviceContent>> {
const deviceId = `${model}-${reference}`;
return lock(`device:${engineId}:${deviceId}`, async () => {
const adminIndexDevice = await this.get(
this.config.adminIndex,
deviceId,
request,
).catch(() => null);

if (!adminIndexDevice) {
return this.create(model, reference, metadata, request);
}

if (
adminIndexDevice._source.engineId &&
adminIndexDevice._source.engineId !== engineId
) {
throw new BadRequestError(
`Device "${adminIndexDevice._id}" already exists on another engine. Abort`,
);
}

const engineDevice = await this.get(engineId, deviceId, request).catch(
() => null,
);

if (!engineDevice) {
await this.attachEngine(engineId, deviceId, request);
}

const updatedPayload = await this.app.trigger<EventDeviceUpdateBefore>(
"device-manager:device:update:before",
{ device: adminIndexDevice, metadata },
);

const updatedDevice = await this.updateDocument<DeviceContent>(
request,
{
_id: deviceId,
_source: { metadata: updatedPayload.metadata },
},
{
collection: InternalCollection.DEVICES,
engineId,
},
{ source: true },
);

await this.app.trigger<EventDeviceUpdateAfter>(
"device-manager:device:update:after",
{
device: updatedDevice,
metadata: updatedPayload.metadata,
},
);

return updatedDevice;
});
}

public async update(
engineId: string,
deviceId: string,
Expand Down
24 changes: 24 additions & 0 deletions lib/modules/device/DevicesController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
ApiDeviceUnlinkAssetResult,
ApiDeviceUpdateResult,
ApiDeviceGetMeasuresResult,
ApiDeviceUpsertResult,
} from "./types/DeviceApi";

export class DevicesController {
Expand Down Expand Up @@ -56,6 +57,12 @@ export class DevicesController {
{ path: "device-manager/:engineId/devices/:_id", verb: "put" },
],
},
upsert: {
handler: this.upsert.bind(this),
http: [
{ path: "device-manager/:engineId/devices/:_id", verb: "post" },
],
},
search: {
handler: this.search.bind(this),
http: [
Expand Down Expand Up @@ -205,6 +212,23 @@ export class DevicesController {
);
}

async upsert(request: KuzzleRequest): Promise<ApiDeviceUpsertResult> {
const engineId = request.getString("engineId");
const model = request.getBodyString("model");
const reference = request.getBodyString("reference");
const metadata = request.getBodyObject("metadata");

const upsertDevice = await this.deviceService.upsert(
engineId,
model,
reference,
metadata,
request,
);

return DeviceSerializer.serialize(upsertDevice);
}

/**
* Create and provision a new device
*/
Expand Down
18 changes: 18 additions & 0 deletions lib/modules/device/types/DeviceApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,24 @@ export interface ApiDeviceUpdateRequest extends DevicesControllerRequest {
}
export type ApiDeviceUpdateResult = KDocument<DeviceContent>;

export interface ApiDeviceUpsertRequest extends DevicesControllerRequest {
action: "upsert";

_id: string;

refresh?: string;

body: {
model: string;

reference: string;

metadata: Metadata;
};
}

export type ApiDeviceUpsertResult = KDocument<DeviceContent>;

export interface ApiDeviceCreateRequest extends DevicesControllerRequest {
action: "create";

Expand Down
128 changes: 128 additions & 0 deletions tests/scenario/migrated/device-upsert.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { beforeEachTruncateCollections } from "../../hooks/collections";
import { beforeAllCreateEngines } from "../../hooks/engines";
import { beforeEachLoadFixtures } from "../../hooks/fixtures";

import { useSdk } from "../../helpers";

jest.setTimeout(10000);

describe("features/Device/Controller/upsert", () => {
const sdk = useSdk();

beforeAll(async () => {
await sdk.connect();
await beforeAllCreateEngines(sdk);
});

beforeEach(async () => {
await beforeEachTruncateCollections(sdk);
await beforeEachLoadFixtures(sdk);
});

afterAll(async () => {
sdk.disconnect();
});
it("Upsert device", async () => {
const response = await sdk.query({
controller: "device-manager/devices",
action: "upsert",
engineId: "engine-kuzzle",
_id: "DummyTemp-detached1",
body: {
model: "DummyTemp",
reference: "detached1",
metadata: { color: 'red' },
},
});

expect(response).toBeDefined();
expect(response.result).toBeDefined();
expect(response.result._id).toEqual("DummyTemp-detached1");
expect(response.result._source.model).toEqual("DummyTemp");
expect(response.result._source.reference).toEqual("detached1");
expect(response.result._source.metadata).toEqual({ color: 'red' });
});

it("Upsert device - update existing device", async () => {
// create device
await sdk.query({
controller: "device-manager/devices",
action: "upsert",
engineId: "engine-kuzzle",
_id: "DummyTemp-detached1",
body: {
model: "DummyTemp",
reference: "detached1",
metadata: { color: 'blue' },
},
});

// update device
const response = await sdk.query({
controller: "device-manager/devices",
action: "upsert",
engineId: "engine-kuzzle",
_id: "DummyTemp-detached1",
body: {
model: "DummyTemp",
reference: "detached1",
metadata: { color: 'red' },
},
});

expect(response).toBeDefined();
expect(response.result).toBeDefined();
expect(response.result._id).toEqual("DummyTemp-detached1");
expect(response.result._source.model).toEqual("DummyTemp");
expect(response.result._source.reference).toEqual("detached1");
expect(response.result._source.metadata).toEqual({ color: 'red' });
})

it('Throws if upsert has no engine id', async () => {

// update asset
let promise
promise = sdk.query({
controller: "device-manager/devices",
action: "upsert",
_id: "DummyTemp-detached1",
body: {
model: "DummyTemp",
reference: "detached1",
metadata: { color: 'red' },
},
});

await expect(promise).rejects.toThrow('Missing argument "engineId"');
})

it('Throws if upsert is on another engine id', async () => {
// create device
await sdk.query({
controller: "device-manager/devices",
action: "upsert",
_id: "DummyTemp-detached1",
engineId: "engine-kuzzle",
body: {
model: "DummyTemp",
reference: "detached1",
metadata: { color: 'blue' },
},
});
// update device
let promise
promise = sdk.query({
controller: "device-manager/devices",
action: "upsert",
engineId: "engine-ayse",
_id: "DummyTemp-detached1",
body: {
model: "DummyTemp",
reference: "detached1",
metadata: { color: 'red' },
},
});

await expect(promise).rejects.toThrow('Device "DummyTemp-detached1" already exists on another engine. Abort');
})
});

0 comments on commit c75abef

Please sign in to comment.