diff --git a/lib/modules/group/GroupsController.ts b/lib/modules/group/GroupsController.ts index 0dad62795..e7519c8a1 100644 --- a/lib/modules/group/GroupsController.ts +++ b/lib/modules/group/GroupsController.ts @@ -2,11 +2,11 @@ import { BadRequestError, ControllerDefinition, EmbeddedSDK, + KuzzleError, KuzzleRequest, NameGenerator, User, } from "kuzzle"; -import { ask } from "kuzzle-plugin-commons"; import { DeviceManagerPlugin, InternalCollection } from "../plugin"; import { @@ -19,19 +19,21 @@ import { ApiGroupDeleteResult, ApiGroupGetResult, ApiGroupListItemsResult, + ApiGroupMCreateResult, + ApiGroupMUpdateResult, + ApiGroupMUpsertResult, ApiGroupRemoveAssetsRequest, ApiGroupRemoveAssetsResult, ApiGroupRemoveDeviceRequest, ApiGroupRemoveDeviceResult, ApiGroupSearchResult, ApiGroupUpdateResult, + ApiGroupUpsertResult, GroupsBodyRequest, } from "./types/GroupsApi"; import { GroupContent } from "./types/GroupContent"; -import { AssetContent } from "../asset"; import { GroupsService } from "./GroupsService"; -import { AskModelGroupGet } from "../model"; -import { DeviceContent } from "../device"; +import { Metadata } from "../shared"; export class GroupsController { definition: ControllerDefinition; @@ -60,6 +62,10 @@ export class GroupsController { handler: this.update.bind(this), http: [{ path: "device-manager/:engineId/groups/:_id", verb: "put" }], }, + upsert: { + handler: this.upsert.bind(this), + http: [{ path: "device-manager/:engineId/groups", verb: "put" }], + }, delete: { handler: this.delete.bind(this), http: [ @@ -127,6 +133,33 @@ export class GroupsController { }, ], }, + mCreate: { + handler: this.mCreate.bind(this), + http: [ + { + path: "device-manager/:engineId/groups/_mCreate", + verb: "post", + }, + ], + }, + mUpdate: { + handler: this.mUpdate.bind(this), + http: [ + { + path: "device-manager/:engineId/groups/_mUpdate", + verb: "put", + }, + ], + }, + mUpsert: { + handler: this.mUpsert.bind(this), + http: [ + { + path: "device-manager/:engineId/groups/_mUpsert", + verb: "put", + }, + ], + }, }, }; /* eslint-enable sort-keys */ @@ -276,164 +309,72 @@ export class GroupsController { return this.groupsService.get(engineId, _id, request); } - async update(request: KuzzleRequest): Promise { + async upsert(request: KuzzleRequest): Promise { const engineId = request.getString("engineId"); - const _id = request.getId(); + const _id = request.getId({ + generator: () => NameGenerator.generateRandomName({ prefix: "group" }), + ifMissing: "generate", + }); const body = request.getBody() as GroupsBodyRequest; const name = body.name; - const path = body.path; + let path = body.path; + const model = body.model; const metadata = body.metadata; - - let updateRequestBody = {}; - if (name !== undefined) { await this.checkGroupName(engineId, name, _id); - updateRequestBody = { ...updateRequestBody, name }; } if (path !== undefined) { await this.checkPath(engineId, path, _id); - updateRequestBody = { ...updateRequestBody, path }; } - - if (metadata !== undefined) { - const group = await this.get(request); - const { model, metadata: groupMetadata } = group._source; - if (model !== null) { - const groupModel = await ask( - "ask:device-manager:model:group:get", - { model }, - ); - for (const metadataName of Object.keys( - groupModel.group.metadataMappings, - )) { - if (metadata[metadataName] !== undefined) { - groupMetadata[metadataName] = metadata[metadataName]; - } - } - updateRequestBody = { ...updateRequestBody, metadata: groupMetadata }; + const group = await this.get(request); + if (!group) { + path = body.path ?? _id; + if (!path.includes(_id)) { + path += `.${_id}`; } + return this.groupsService.create( + _id, + engineId, + metadata, + model, + name, + path, + request, + ); } - - const groupToUpdate = await this.sdk.document.get( - engineId, - InternalCollection.GROUPS, - _id, - ); - const updatedGroup = await this.groupsService.update( + return this.groupsService.update( request, _id, engineId, - updateRequestBody, + name, + path, + metadata, ); + } - if (updatedGroup._source.path !== groupToUpdate._source.path) { - const { hits: assets } = await this.sdk.document.search( - engineId, - InternalCollection.ASSETS, - { - query: { - prefix: { - "groups.path": { - value: groupToUpdate._source.path, - }, - }, - }, - }, - { lang: "koncorde" }, - ); - const { hits: devices } = await this.sdk.document.search( - engineId, - InternalCollection.DEVICES, - { - query: { - prefix: { - "groups.path": { - value: groupToUpdate._source.path, - }, - }, - }, - }, - { lang: "koncorde" }, - ); - await this.sdk.document.mUpdate( - engineId, - InternalCollection.ASSETS, - assets.map((asset) => ({ - _id: asset._id, - body: { - groups: asset._source.groups.map((grp) => { - if (grp.path.includes(groupToUpdate._source.path)) { - grp.path = grp.path.replace( - groupToUpdate._source.path, - updatedGroup._source.path, - ); - grp.date = Date.now(); - } - return grp; - }), - }, - })), - { strict: true }, - ); - await this.sdk.document.mUpdate( - engineId, - InternalCollection.DEVICES, - devices.map((device) => ({ - _id: device._id, - body: { - groups: device._source.groups.map((grp) => { - if (grp.path.includes(groupToUpdate._source.path)) { - grp.path = grp.path.replace( - groupToUpdate._source.path, - updatedGroup._source.path, - ); - grp.date = Date.now(); - } - return grp; - }), - }, - })), - { strict: true }, - ); - const { hits: childrenGroups } = - await this.sdk.document.search( - engineId, - InternalCollection.GROUPS, - { - query: { - and: [ - { - prefix: { - path: { - value: groupToUpdate._source.path, - }, - }, - }, - { - not: { equals: { path: groupToUpdate._source.path } }, - }, - ], - }, - }, - { lang: "koncorde" }, - ); + async update(request: KuzzleRequest): Promise { + const engineId = request.getString("engineId"); + const _id = request.getId(); + const body = request.getBody() as GroupsBodyRequest; + const name = body.name; + const path = body.path; + const metadata = body.metadata; + if (name !== undefined) { + await this.checkGroupName(engineId, name, _id); + } - await this.sdk.document.mUpdate( - engineId, - InternalCollection.GROUPS, - childrenGroups.map((grp) => { - grp._source.path = grp._source.path.replace( - groupToUpdate._source.path, - updatedGroup._source.path, - ); - grp._source.lastUpdate = Date.now(); - return { _id: grp._id, body: grp._source }; - }), - { strict: true }, - ); + if (path !== undefined) { + await this.checkPath(engineId, path, _id); } - return updatedGroup; + return this.groupsService.update( + request, + _id, + engineId, + name, + path, + metadata, + ); } async delete(request: KuzzleRequest): Promise { @@ -502,4 +443,183 @@ export class GroupsController { this.checkPath(engineId, path); return this.groupsService.removeDevice(engineId, path, deviceIds, request); } + + async mCreate(request: KuzzleRequest): Promise { + const engineId = request.getString("engineId"); + const groups = request.getBodyArray("groups"); + const errors = []; + const promises: Array<() => Promise> = []; + const toCreate: Array<{ + _id: string; + metadata: Metadata; + model: string; + name: string; + path: string; + }> = []; + for (const g of groups) { + const name = g.name; + const metadata = g.metadata ?? {}; + const model = g.model ?? null; + + const _id = + g._id ?? NameGenerator.generateRandomName({ prefix: "group" }); + let path = g.path ?? _id; + if (!path.includes(_id)) { + path += `.${_id}`; + } + promises.push(async () => { + try { + if (typeof name !== "string") { + throw new BadRequestError(`A group must have a name`); + } + await this.checkPath(engineId, path, _id); + + await this.checkGroupName(engineId, name); + + toCreate.push({ _id, metadata, model, name, path }); + } catch (error) { + let reason: string = ""; + let status: number = 400; + if (error instanceof KuzzleError) { + reason = error.message; + status = error.code; + } + errors.push({ + document: { _id, body: { metadata, model, name, path } }, + reason, + status, + }); + } + }); + } + await Promise.allSettled( + promises.map((f) => new Promise((resolve) => f().then(resolve))), + ); + const res = await this.groupsService.mCreate(engineId, toCreate); + return { + errors: [...res.errors, ...errors], + successes: res.successes, + }; + } + + async mUpdate(request: KuzzleRequest): Promise { + const engineId = request.getString("engineId"); + const groups = request.getBodyArray("groups"); + const errors = []; + const promises: Array<() => Promise> = []; + const toUpdate: Array<{ + _id: string; + metadata: Metadata; + model: string; + name: string; + path: string; + }> = []; + for (const g of groups) { + const name = g.name; + const metadata = g.metadata ?? {}; + const model = g.model ?? null; + + const _id = g._id; + const path = g.path; + promises.push(async () => { + try { + if (typeof _id !== "string") { + throw new BadRequestError(`A group must have an _id`); + } + if (name) { + if (typeof name !== "string" || name.trim() === "") { + throw new BadRequestError( + `A group name must be a non-empty string`, + ); + } + await this.checkGroupName(engineId, name, _id); + } + + if (path) { + await this.checkPath(engineId, path, _id); + } + toUpdate.push({ _id, metadata, model, name, path }); + } catch (error) { + let reason: string = ""; + let status: number = 400; + if (error instanceof KuzzleError) { + reason = error.message; + status = error.code; + } + errors.push({ + document: { _id, body: { metadata, model, name, path } }, + reason, + status, + }); + } + }); + } + await Promise.allSettled( + promises.map((f) => new Promise((resolve) => f().then(resolve))), + ); + const res = await this.groupsService.mUpdate(engineId, toUpdate, request); + return { errors: [...res.errors, ...errors], successes: res.successes }; + } + async mUpsert(request: KuzzleRequest): Promise { + const engineId = request.getString("engineId"); + const groups = request.getBodyArray("groups"); + const errors = []; + const promises: Array<() => Promise> = []; + const toUpsert: Array<{ + _id: string; + metadata: Metadata; + model: string; + name: string; + path: string; + }> = []; + for (const g of groups) { + const name = g.name; + const metadata = g.metadata ?? {}; + const model = g.model ?? null; + + const _id = + g._id ?? NameGenerator.generateRandomName({ prefix: "group" }); + let path: string = g.path; + promises.push(async () => { + try { + if (typeof _id !== "string") { + throw new BadRequestError(`A group must have an _id`); + } + if (name) { + if (typeof name !== "string" || name.trim() === "") { + throw new BadRequestError( + `A group name must be a non-empty string`, + ); + } + await this.checkGroupName(engineId, name, _id); + } + + if (path) { + if (!g._id) { + path += `.${_id}`; + } + await this.checkPath(engineId, path, _id); + } + toUpsert.push({ _id, metadata, model, name, path }); + } catch (error) { + let reason: string = ""; + let status: number = 400; + if (error instanceof KuzzleError) { + reason = error.message; + status = error.code; + } + errors.push({ + document: { _id, body: { metadata, model, name, path } }, + reason, + status, + }); + } + }); + } + await Promise.allSettled( + promises.map((f) => new Promise((resolve) => f().then(resolve))), + ); + const res = await this.groupsService.mUpsert(engineId, toUpsert, request); + return { errors: [...errors, ...res.errors], successes: res.successes }; + } } diff --git a/lib/modules/group/GroupsService.ts b/lib/modules/group/GroupsService.ts index d7d2ff9ea..99c811f95 100644 --- a/lib/modules/group/GroupsService.ts +++ b/lib/modules/group/GroupsService.ts @@ -1,12 +1,17 @@ import { JSONObject, KDocument } from "kuzzle-sdk"; import { DeviceManagerPlugin, InternalCollection } from "../plugin"; -import { BaseService, SearchParams } from "../shared"; -import { BadRequestError, KuzzleRequest } from "kuzzle"; +import { BaseService, Metadata, SearchParams } from "../shared"; +import { BadRequestError, KuzzleError, KuzzleRequest } from "kuzzle"; import { AskModelGroupGet, GroupModelContent } from "../model"; import { ask } from "kuzzle-plugin-commons"; import { AssetContent } from "../asset/types/AssetContent"; -import { GroupContent } from "./exports"; +import { + ApiGroupMCreateResult, + ApiGroupMUpdateResult, + ApiGroupMUpsertResult, + GroupContent, +} from "./exports"; import { DeviceContent } from "../device"; export class GroupsService extends BaseService { @@ -70,18 +75,157 @@ export class GroupsService extends BaseService { request: KuzzleRequest, _id: string, engineId: string, - updateContent: JSONObject, + name?: string, + path?: string, + metadata?: Metadata, ) { - const updatedGroup = await this.updateDocument( + let updateRequestBody = {}; + + if (name !== undefined) { + updateRequestBody = { ...updateRequestBody, name }; + } + + if (path !== undefined) { + updateRequestBody = { ...updateRequestBody, path }; + } + + if (metadata !== undefined) { + const group = await this.get(engineId, _id, request); + const { model, metadata: groupMetadata } = group._source; + if (model !== null) { + const groupModel = await ask( + "ask:device-manager:model:group:get", + { model }, + ); + for (const metadataName of Object.keys( + groupModel.group.metadataMappings, + )) { + if (metadata[metadataName] !== undefined) { + groupMetadata[metadataName] = metadata[metadataName]; + } + } + updateRequestBody = { ...updateRequestBody, metadata: groupMetadata }; + } + } + + const groupToUpdate = await this.sdk.document.get( + engineId, + InternalCollection.GROUPS, + _id, + ); + const updatedGroup = await this._update( request, - { - _id, - _source: { ...updateContent, lastUpdate: Date.now() }, - }, - { collection: InternalCollection.GROUPS, engineId }, - { source: true, triggerEvents: true }, + _id, + engineId, + updateRequestBody, ); + if (updatedGroup._source.path !== groupToUpdate._source.path) { + const { hits: assets } = await this.sdk.document.search( + engineId, + InternalCollection.ASSETS, + { + query: { + prefix: { + "groups.path": { + value: groupToUpdate._source.path, + }, + }, + }, + }, + { lang: "koncorde" }, + ); + const { hits: devices } = await this.sdk.document.search( + engineId, + InternalCollection.DEVICES, + { + query: { + prefix: { + "groups.path": { + value: groupToUpdate._source.path, + }, + }, + }, + }, + { lang: "koncorde" }, + ); + await this.sdk.document.mUpdate( + engineId, + InternalCollection.ASSETS, + assets.map((asset) => ({ + _id: asset._id, + body: { + groups: asset._source.groups.map((grp) => { + if (grp.path.includes(groupToUpdate._source.path)) { + grp.path = grp.path.replace( + groupToUpdate._source.path, + updatedGroup._source.path, + ); + grp.date = Date.now(); + } + return grp; + }), + }, + })), + { strict: true }, + ); + await this.sdk.document.mUpdate( + engineId, + InternalCollection.DEVICES, + devices.map((device) => ({ + _id: device._id, + body: { + groups: device._source.groups.map((grp) => { + if (grp.path.includes(groupToUpdate._source.path)) { + grp.path = grp.path.replace( + groupToUpdate._source.path, + updatedGroup._source.path, + ); + grp.date = Date.now(); + } + return grp; + }), + }, + })), + { strict: true }, + ); + const { hits: childrenGroups } = + await this.sdk.document.search( + engineId, + InternalCollection.GROUPS, + { + query: { + and: [ + { + prefix: { + path: { + value: groupToUpdate._source.path, + }, + }, + }, + { + not: { equals: { path: groupToUpdate._source.path } }, + }, + ], + }, + }, + { lang: "koncorde" }, + ); + + await this.sdk.document.mUpdate( + engineId, + InternalCollection.GROUPS, + childrenGroups.map((grp) => { + grp._source.path = grp._source.path.replace( + groupToUpdate._source.path, + updatedGroup._source.path, + ); + grp._source.lastUpdate = Date.now(); + return { _id: grp._id, body: grp._source }; + }), + { strict: true }, + ); + } return updatedGroup; } @@ -159,6 +303,7 @@ export class GroupsService extends BaseService { engineId, }); } + async search( engineId: string, searchParams: SearchParams, @@ -280,7 +425,7 @@ export class GroupsService extends BaseService { return asset; }); - const groupUpdate = await this.update(request, _id, engineId, { + const groupUpdate = await this._update(request, _id, engineId, { lastUpdate: Date.now(), }); @@ -296,6 +441,7 @@ export class GroupsService extends BaseService { group: groupUpdate, }; } + async removeAsset( engineId: string, path: string, @@ -330,7 +476,7 @@ export class GroupsService extends BaseService { return asset; }); - const groupUpdate = await this.update(request, _id, engineId, { + const groupUpdate = await this._update(request, _id, engineId, { lastUpdate: Date.now(), }); @@ -346,6 +492,7 @@ export class GroupsService extends BaseService { group: groupUpdate, }; } + async addDevice( engineId: string, path: string, @@ -410,7 +557,7 @@ export class GroupsService extends BaseService { return device; }); - const groupUpdate = await this.update(request, _id, engineId, { + const groupUpdate = await this._update(request, _id, engineId, { lastUpdate: Date.now(), }); @@ -429,6 +576,7 @@ export class GroupsService extends BaseService { group: groupUpdate, }; } + async removeDevice( engineId: string, path: string, @@ -463,7 +611,7 @@ export class GroupsService extends BaseService { return device; }); - const groupUpdate = await this.update(request, _id, engineId, { + const groupUpdate = await this._update(request, _id, engineId, { lastUpdate: Date.now(), }); @@ -482,4 +630,182 @@ export class GroupsService extends BaseService { group: groupUpdate, }; } + + async mCreate( + engineId: string, + groups: Array<{ + _id: string; + metadata: Metadata; + model: string; + name: string; + path: string; + }>, + ): Promise { + const toCreate = []; + const toCreateErrors = []; + for (const g of groups) { + try { + const groupMetadata = {}; + if (g.model !== null) { + const groupModel = await ask( + "ask:device-manager:model:group:get", + { model: g.model }, + ); + for (const metadataName of Object.keys( + groupModel.group.metadataMappings, + )) { + if (g.metadata[metadataName]) { + groupMetadata[metadataName] = g.metadata[metadataName]; + } else if (groupModel.group.defaultMetadata[metadataName]) { + groupMetadata[metadataName] = + groupModel.group.defaultMetadata[metadataName]; + } else { + groupMetadata[metadataName] = null; + } + } + } + toCreate.push({ + _id: g._id, + body: { + lastUpdate: Date.now(), + metadata: { ...groupMetadata }, + model: g.model, + name: g.name, + path: g.path, + }, + }); + } catch (error) { + let reason: string = ""; + let status: number = 400; + if (error instanceof KuzzleError) { + reason = error.message; + status = error.code; + } + toCreateErrors.push({ + document: { + _id: g._id, + body: { + metadata: g.metadata, + model: g.model, + name: g.name, + path: g.path, + }, + }, + reason, + status, + }); + } + } + const { successes, errors } = await this.sdk.document.mCreate( + engineId, + InternalCollection.GROUPS, + toCreate, + ); + return { + errors: [...toCreateErrors, ...errors], + successes: successes, + } as ApiGroupMCreateResult; + } + + async mUpdate( + engineId: string, + groups: Array<{ + _id: string; + metadata: Metadata; + model: string; + name: string; + path: string; + }>, + request: KuzzleRequest, + ): Promise { + const successes: KDocument[] = []; + const errors: ApiGroupMUpdateResult["errors"] = []; + await Promise.allSettled( + groups.map((g) => + this.update(request, g._id, engineId, g.name, g.path, g.metadata) + .then((u) => successes.push(u)) + .catch((error) => { + const { _id, ...rest } = g; + let reason: string = ""; + let status: number = 400; + if (error instanceof KuzzleError) { + reason = error.message; + status = error.code; + } + errors.push({ + document: { _id, body: { ...rest } }, + reason, + status, + }); + }), + ), + ); + return { + errors, + successes, + }; + } + async mUpsert( + engineId: string, + groups: Array<{ + _id: string; + metadata: Metadata; + model: string; + name: string; + path: string; + }>, + request: KuzzleRequest, + ): Promise { + const toUpdate: Array<{ + _id: string; + metadata: Metadata; + model: string; + name: string; + path: string; + }> = []; + const toCreate: Array<{ + _id: string; + metadata: Metadata; + model: string; + name: string; + path: string; + }> = []; + for (const g of groups) { + try { + await this.getDocument(request, g._id, { + collection: InternalCollection.GROUPS, + engineId, + }); + toUpdate.push(g); + } catch { + g.path = g.path ?? g._id; + if (!g.path.includes(g._id)) { + g.path += `.${g._id}`; + } + toCreate.push(g); + } + } + const createdResult = await this.mCreate(engineId, toCreate); + const updatedResult = await this.mUpdate(engineId, toUpdate, request); + return { + errors: [...createdResult.errors, ...updatedResult.errors], + successes: [...createdResult.successes, ...updatedResult.successes], + }; + } + private async _update( + request: KuzzleRequest, + _id: string, + engineId: string, + updateContent: JSONObject, + ) { + return this.updateDocument( + request, + { + _id, + _source: { ...updateContent, lastUpdate: Date.now() }, + }, + { collection: InternalCollection.GROUPS, engineId }, + { source: true, triggerEvents: true }, + ); + } } diff --git a/lib/modules/group/types/GroupsApi.ts b/lib/modules/group/types/GroupsApi.ts index 839297a23..c171e0ecb 100644 --- a/lib/modules/group/types/GroupsApi.ts +++ b/lib/modules/group/types/GroupsApi.ts @@ -3,6 +3,7 @@ import { KDocument, KHit, SearchResult, + mCreateResponse, mUpdateResponse, } from "kuzzle-sdk"; import { GroupsBody, GroupContent } from "./GroupContent"; @@ -45,6 +46,13 @@ export interface ApiGroupUpdateRequest extends GroupControllerRequest { export type ApiGroupUpdateResult = KDocument; +export interface ApiGroupUpsertRequest extends GroupControllerRequest { + action: "upsert"; + _id?: string; + body: GroupsBodyRequest; +} + +export type ApiGroupUpsertResult = KDocument; export interface ApiGroupDeleteRequest extends GroupControllerRequest { action: "delete"; _id: string; @@ -108,3 +116,44 @@ export interface ApiGroupRemoveDeviceRequest extends GroupControllerRequest { }; } export type ApiGroupRemoveDeviceResult = UpdateLinkResponse; + +export interface ApiGroupMCreateRequest extends GroupControllerRequest { + action: "mCreate"; + body: { + groups: Array; + }; +} +export type ApiGroupMCreateResult = mCreateResponse & { + successes: Array<{ + _id: string; + _source: GroupContent; + created: boolean; + }>; +}; + +export interface ApiGroupMUpdateRequest extends GroupControllerRequest { + action: "mUpdate"; + body: { + groups: Array; + }; +} +export type ApiGroupMUpdateResult = { + errors: mUpdateResponse["errors"]; + successes: KDocument[]; +}; + +export interface ApiGroupMUpsertRequest extends GroupControllerRequest { + action: "mUpsert"; + body: { + groups: Array; + }; +} +export type ApiGroupMUpsertResult = { + errors: Array< + ApiGroupMCreateResult["errors"][0] | ApiGroupMUpdateResult["errors"][0] + >; + successes: Array< + | ApiGroupMCreateResult["successes"][0] + | ApiGroupMUpdateResult["successes"][0] + >; +}; diff --git a/package-lock.json b/package-lock.json index ff808fd77..4df2b5ea9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,7 @@ "execa": "9.5.2", "jest": "29.7.0", "kuzzle": "2.40.0", - "kuzzle-sdk": "7.15.0", + "kuzzle-sdk": "7.15.1", "read-pkg": "9.0.1", "semantic-release-config-kuzzle": "1.1.2", "semantic-release-slack-bot": "4.0.2", @@ -12036,9 +12036,9 @@ } }, "node_modules/kuzzle-sdk": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/kuzzle-sdk/-/kuzzle-sdk-7.15.0.tgz", - "integrity": "sha512-kAACu+hr4kUts9lfeAa+eAowCO1jE/C4KGsLvopICnmHoMQMC1QIzeIfwRAeA6ZG7d1XRpw9Ajr4RxfUARLhZA==", + "version": "7.15.1", + "resolved": "https://registry.npmjs.org/kuzzle-sdk/-/kuzzle-sdk-7.15.1.tgz", + "integrity": "sha512-RG3MdqVVGWB/b6cOyLBH1Kw3zGnpxi5M143H8FmhH9m6GgS60r5zD8ftbl6BRa8XkXGW68qE5f54eu7wFTpBnQ==", "license": "Apache-2.0", "dependencies": { "min-req-promise": "^1.0.1", diff --git a/package.json b/package.json index 57337ff63..6721b5ddd 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "execa": "9.5.2", "jest": "29.7.0", "kuzzle": "2.40.0", - "kuzzle-sdk": "7.15.0", + "kuzzle-sdk": "7.15.1", "read-pkg": "9.0.1", "semantic-release-config-kuzzle": "1.1.2", "semantic-release-slack-bot": "4.0.2", diff --git a/tests/scenario/modules/groups/group.test.ts b/tests/scenario/modules/groups/group.test.ts index d513f59fd..c41aeb9b1 100644 --- a/tests/scenario/modules/groups/group.test.ts +++ b/tests/scenario/modules/groups/group.test.ts @@ -30,6 +30,12 @@ import { ApiGroupRemoveDeviceResult, ApiGroupListItemsRequest, ApiGroupListItemsResult, + ApiGroupMCreateRequest, + ApiGroupMCreateResult, + ApiGroupMUpdateRequest, + ApiGroupMUpdateResult, + ApiGroupMUpsertRequest, + ApiGroupMUpsertResult, } from "../../../../lib/modules/group/types/GroupsApi"; import { AssetContent } from "../../../../lib/modules/asset/exports"; import { InternalCollection } from "../../../../lib/modules/plugin"; @@ -948,4 +954,207 @@ describe("GroupsController", () => { }, }); }); + + it("can create multiple groups at once", async () => { + const queryWithError: ApiGroupMCreateRequest = { + controller: "device-manager/groups", + engineId: "engine-ayse", + action: "mCreate", + body: { + groups: [ + { name: "no-parents" }, + { + name: "parking with model", + model: "DeviceRestricted", + }, + { name: "test child", path: groupTestId }, + // ERRORS + { name: "id taken", _id: groupTestId }, + { name: "wrong path", path: "wrong.path" }, + //Name already taken + { name: "Test group" }, + ], + }, + }; + + const { result } = await sdk.query< + ApiGroupMCreateRequest, + ApiGroupMCreateResult + >(queryWithError); + expect(result).toHaveProperty("successes"); + expect(result).toHaveProperty("errors"); + expect(result.successes).toHaveLength(3); + expect(result.errors).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + reason: "document already exists", + document: expect.objectContaining({ + body: expect.objectContaining({ name: "id taken" }), + }), + }), + expect.objectContaining({ + reason: 'A group with name "Test group" already exist', + document: expect.objectContaining({ + body: expect.objectContaining({ name: "Test group" }), + }), + }), + expect.objectContaining({ + reason: 'The closest parent group "path" does not exist', + document: expect.objectContaining({ + body: expect.objectContaining({ name: "wrong path" }), + }), + }), + ]), + ); + }); + + it("can update multiple groups at once", async () => { + const missingIdBody = { name: "missing id" }; + const badParentId = { + _id: groupTestId, + name: "bad parent", + path: "not-exist." + groupTestId, + }; + const duplicateGroupNameBody = { + _id: groupTestParentId1, + name: "test group", + path: groupTestParentId1, + }; + const updateNameBody = { + _id: groupTestId, + name: "root group", + path: groupTestId, + }; + const mUpdateQuery = { + controller: "device-manager/groups", + engineId: "engine-ayse", + action: "mUpdate", + body: { + groups: [ + missingIdBody, + badParentId, + duplicateGroupNameBody, + updateNameBody, + ], + }, + } as ApiGroupMUpdateRequest; + + const { result } = await sdk.query< + ApiGroupMUpdateRequest, + ApiGroupMUpdateResult + >(mUpdateQuery); + + expect(result).toHaveProperty("successes"); + expect(result).toHaveProperty("errors"); + + expect(result.errors).toHaveLength(3); + expect(result.successes).toHaveLength(1); + expect(result.errors).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + reason: 'The closest parent group "not-exist" does not exist', + document: expect.objectContaining({ + body: expect.objectContaining({ name: "bad parent" }), + }), + }), + expect.objectContaining({ + reason: "A group must have an _id", + document: expect.objectContaining({ + body: expect.objectContaining({ name: "missing id" }), + }), + }), + expect.objectContaining({ + reason: 'A group with name "test group" already exist', + document: expect.objectContaining({ + body: expect.objectContaining({ name: "test group" }), + }), + }), + ]), + ); + }); + + it("can upsert multiple groups at once", async () => { + const createNoParent = { name: "no-parents" }; + const createWithModel = { + name: "parking with model", + model: "DeviceRestricted", + }; + const createWithParent = { name: "test child", path: groupTestId }; + const updateNameBody = { + _id: groupTestId, + name: "root group", + path: groupTestId, + }; + // ERRORS + const createBadPath = { name: "wrong path", path: "wrong.path" }; + //Name already taken + const createNameTaken = { name: "Test group" }; + const updateBadParent = { + _id: groupTestId, + name: "bad parent", + path: "not-exist." + groupTestId, + }; + const updateNameTaken = { + _id: groupTestParentId1, + name: "test group", + path: groupTestParentId1, + }; + + const mUpsertQuery = { + controller: "device-manager/groups", + engineId: "engine-ayse", + action: "mUpsert", + body: { + groups: [ + createNoParent, + createWithModel, + createWithParent, + createBadPath, + createNameTaken, + updateBadParent, + updateNameTaken, + updateNameBody, + ], + }, + } as ApiGroupMUpsertRequest; + + const { result } = await sdk.query< + ApiGroupMUpsertRequest, + ApiGroupMUpsertResult + >(mUpsertQuery); + + expect(result).toHaveProperty("successes"); + expect(result).toHaveProperty("errors"); + expect(result.successes).toHaveLength(4); + + expect(result.errors).toHaveLength(4); + expect(result.errors).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + reason: 'The closest parent group "not-exist" does not exist', + document: expect.objectContaining({ + body: expect.objectContaining({ name: "bad parent" }), + }), + }), + expect.objectContaining({ + reason: 'A group with name "Test group" already exist', + document: expect.objectContaining({ + body: expect.objectContaining({ name: "Test group" }), + }), + }), + expect.objectContaining({ + reason: 'A group with name "test group" already exist', + document: expect.objectContaining({ + body: expect.objectContaining({ name: "test group" }), + }), + }), + expect.objectContaining({ + reason: 'The closest parent group "path" does not exist', + document: expect.objectContaining({ + body: expect.objectContaining({ name: "wrong path" }), + }), + }), + ]), + ); + }); });