kuzzle-device-manager 3.0.0-3-dev.20 → 3.0.0-3-dev.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/lib/modules/asset/AssetService.d.ts +6 -0
  2. package/dist/lib/modules/asset/AssetService.js +46 -46
  3. package/dist/lib/modules/asset/AssetService.js.map +1 -1
  4. package/dist/lib/modules/asset/roles/RoleAssetsAdmin.d.ts +1 -0
  5. package/dist/lib/modules/asset/roles/RoleAssetsAdmin.js +2 -0
  6. package/dist/lib/modules/asset/roles/RoleAssetsAdmin.js.map +1 -1
  7. package/dist/lib/modules/asset/roles/RoleAssetsReader.d.ts +1 -0
  8. package/dist/lib/modules/asset/roles/RoleAssetsReader.js +2 -0
  9. package/dist/lib/modules/asset/roles/RoleAssetsReader.js.map +1 -1
  10. package/dist/lib/modules/device/DeviceService.js +23 -23
  11. package/dist/lib/modules/device/DeviceService.js.map +1 -1
  12. package/dist/lib/modules/device/roles/RoleDevicesAdmin.d.ts +1 -0
  13. package/dist/lib/modules/device/roles/RoleDevicesAdmin.js +2 -0
  14. package/dist/lib/modules/device/roles/RoleDevicesAdmin.js.map +1 -1
  15. package/dist/lib/modules/device/roles/RoleDevicesReader.d.ts +1 -0
  16. package/dist/lib/modules/device/roles/RoleDevicesReader.js +2 -0
  17. package/dist/lib/modules/device/roles/RoleDevicesReader.js.map +1 -1
  18. package/dist/lib/modules/group/GroupsService.d.ts +5 -0
  19. package/dist/lib/modules/group/GroupsService.js +39 -0
  20. package/dist/lib/modules/group/GroupsService.js.map +1 -1
  21. package/dist/lib/modules/group/index.d.ts +1 -0
  22. package/dist/lib/modules/group/index.js +1 -0
  23. package/dist/lib/modules/group/index.js.map +1 -1
  24. package/dist/lib/modules/group/roles/RoleGroupsAdmin.d.ts +1 -0
  25. package/dist/lib/modules/group/roles/RoleGroupsAdmin.js +2 -0
  26. package/dist/lib/modules/group/roles/RoleGroupsAdmin.js.map +1 -1
  27. package/dist/lib/modules/group/roles/RoleGroupsReader.d.ts +1 -0
  28. package/dist/lib/modules/group/roles/RoleGroupsReader.js +2 -0
  29. package/dist/lib/modules/group/roles/RoleGroupsReader.js.map +1 -1
  30. package/dist/lib/modules/group/types/GroupEvents.d.ts +8 -0
  31. package/dist/lib/modules/group/types/GroupEvents.js +3 -0
  32. package/dist/lib/modules/group/types/GroupEvents.js.map +1 -0
  33. package/dist/lib/modules/model/MetadataReferential.d.ts +119 -0
  34. package/dist/lib/modules/model/MetadataReferential.js +342 -0
  35. package/dist/lib/modules/model/MetadataReferential.js.map +1 -0
  36. package/dist/lib/modules/model/ModelService.d.ts +103 -1
  37. package/dist/lib/modules/model/ModelService.js +486 -185
  38. package/dist/lib/modules/model/ModelService.js.map +1 -1
  39. package/dist/lib/modules/model/ModelsConflicts.d.ts +23 -0
  40. package/dist/lib/modules/model/ModelsConflicts.js +54 -4
  41. package/dist/lib/modules/model/ModelsConflicts.js.map +1 -1
  42. package/dist/lib/modules/model/ModelsController.d.ts +11 -2
  43. package/dist/lib/modules/model/ModelsController.js +109 -11
  44. package/dist/lib/modules/model/ModelsController.js.map +1 -1
  45. package/dist/lib/modules/model/ModelsRegister.d.ts +33 -1
  46. package/dist/lib/modules/model/ModelsRegister.js +101 -3
  47. package/dist/lib/modules/model/ModelsRegister.js.map +1 -1
  48. package/dist/lib/modules/model/collections/modelsMappings.js +7 -0
  49. package/dist/lib/modules/model/collections/modelsMappings.js.map +1 -1
  50. package/dist/lib/modules/model/types/ModelApi.d.ts +93 -2
  51. package/dist/lib/modules/model/types/ModelContent.d.ts +67 -1
  52. package/dist/lib/modules/model/types/ModelContent.js.map +1 -1
  53. package/dist/lib/modules/model/types/ModelDefinition.d.ts +6 -3
  54. package/dist/lib/modules/model/types/ModelEvents.d.ts +6 -1
  55. package/dist/lib/modules/plugin/DeviceManagerEngine.d.ts +33 -0
  56. package/dist/lib/modules/plugin/DeviceManagerEngine.js +106 -10
  57. package/dist/lib/modules/plugin/DeviceManagerEngine.js.map +1 -1
  58. package/dist/lib/modules/plugin/DeviceManagerPlugin.d.ts +36 -1
  59. package/dist/lib/modules/plugin/DeviceManagerPlugin.js +42 -0
  60. package/dist/lib/modules/plugin/DeviceManagerPlugin.js.map +1 -1
  61. package/dist/lib/modules/plugin/types/DeviceManagerConfiguration.d.ts +11 -0
  62. package/dist/lib/modules/shared/utils/lock.d.ts +7 -1
  63. package/dist/lib/modules/shared/utils/lock.js +2 -2
  64. package/dist/lib/modules/shared/utils/lock.js.map +1 -1
  65. package/package.json +1 -1
@@ -1,4 +1,7 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.ModelService = void 0;
4
7
  const kuzzle_1 = require("kuzzle");
@@ -11,6 +14,8 @@ const AJValidator_1 = require("../shared/utils/AJValidator");
11
14
  const SchemaValidationError_1 = require("../shared/errors/SchemaValidationError");
12
15
  const MeasuresDuplicates_1 = require("./MeasuresDuplicates");
13
16
  const MeasuresNamesDuplicatesError_1 = require("./MeasuresNamesDuplicatesError");
17
+ const lodash_1 = __importDefault(require("lodash"));
18
+ const MetadataReferential_1 = require("./MetadataReferential");
14
19
  class ModelService extends shared_1.BaseService {
15
20
  constructor(plugin, logger) {
16
21
  super(plugin);
@@ -30,6 +35,7 @@ class ModelService extends shared_1.BaseService {
30
35
  const groupModel = await this.getGroup(model);
31
36
  return groupModel._source;
32
37
  });
38
+ (0, kuzzle_plugin_commons_1.onAsk)("ask:device-manager:model:metadata-referential:get", async () => this.getMetadataReferential());
33
39
  (0, kuzzle_plugin_commons_1.onAsk)("ask:device-manager:model:measure:get", async ({ type, index }) => {
34
40
  const measureModel = await this.getMeasure(type, index);
35
41
  return measureModel._source;
@@ -156,157 +162,177 @@ class ModelService extends shared_1.BaseService {
156
162
  return { models, strict, type };
157
163
  }
158
164
  async writeAsset(tenantClasses, model, metadataMappings, defaultMetadata, metadataDetails, metadataGroups, measures, tooltipModels, locales, indexes, icon) {
159
- if (kuzzle_1.Inflector.pascalCase(model) !== model) {
160
- throw new kuzzle_1.BadRequestError(`Asset model "${model}" must be PascalCase.`);
161
- }
162
- if (indexes?.length && tenantClasses?.length) {
163
- throw new kuzzle_1.BadRequestError(`"indexes" and "tenantClasses" are mutually exclusive on an asset model.`);
164
- }
165
- const duplicates = (0, MeasuresDuplicates_1.getNamedMeasuresDuplicates)(measures);
166
- if (duplicates.length > 0) {
167
- throw new MeasuresNamesDuplicatesError_1.MeasuresNamesDuplicatesError("Asset model measures contain one or multiple duplicate measure name", duplicates);
168
- }
169
- const normalizedTenantClasses = tenantClasses.includes("commons")
170
- ? ["commons"]
171
- : tenantClasses;
172
- // Anti-shadowing: reject if a model with the same name exists at a different scope level
173
- const isGlobal = normalizedTenantClasses.includes("commons") &&
174
- (!indexes || indexes.length === 0);
175
- const isTenantScoped = indexes && indexes.length > 0;
176
- const isGroupScoped = !isGlobal && !isTenantScoped;
177
- await this.sdk.collection.refresh(this.config.platformIndex, plugin_1.InternalCollection.MODELS);
178
- const existing = await this.sdk.document.search(this.config.platformIndex, plugin_1.InternalCollection.MODELS, {
179
- query: {
180
- bool: {
181
- must: [
182
- { term: { type: "asset" } },
183
- { term: { "asset.model": model } },
184
- ],
185
- },
186
- },
187
- }, { size: 10 });
188
- for (const hit of existing.hits) {
189
- const doc = hit._source;
190
- const docIsGlobal = doc.tenantClasses?.includes("commons") &&
191
- (!doc.indexes || doc.indexes.length === 0);
192
- const docIsTenantScoped = doc.indexes && doc.indexes.length > 0;
193
- const docIsGroupScoped = !docIsGlobal && !docIsTenantScoped;
194
- // Same scope level: allow overwrite
195
- if (isGlobal && docIsGlobal) {
196
- continue;
197
- }
198
- if (isTenantScoped && docIsTenantScoped) {
199
- continue;
200
- }
201
- if (isGroupScoped && docIsGroupScoped) {
202
- continue;
165
+ return (0, MetadataReferential_1.withMetadataReferentialLock)(async () => {
166
+ if (kuzzle_1.Inflector.pascalCase(model) !== model) {
167
+ throw new kuzzle_1.BadRequestError(`Asset model "${model}" must be PascalCase.`);
203
168
  }
204
- // Different scope levels: reject
205
- if (docIsGlobal) {
206
- throw new kuzzle_1.BadRequestError(`Asset model "${model}" already exists as a global (commons) model. A model cannot exist at multiple scopes.`);
169
+ if (indexes?.length && tenantClasses?.length) {
170
+ throw new kuzzle_1.BadRequestError(`"indexes" and "tenantClasses" are mutually exclusive on an asset model.`);
207
171
  }
208
- if (isGlobal) {
209
- throw new kuzzle_1.BadRequestError(`Asset model "${model}" already exists at a non-global scope. A model cannot exist at multiple scopes.`);
172
+ const duplicates = (0, MeasuresDuplicates_1.getNamedMeasuresDuplicates)(measures);
173
+ if (duplicates.length > 0) {
174
+ throw new MeasuresNamesDuplicatesError_1.MeasuresNamesDuplicatesError("Asset model measures contain one or multiple duplicate measure name", duplicates);
210
175
  }
211
- if (isTenantScoped && docIsGroupScoped) {
212
- throw new kuzzle_1.BadRequestError(`Asset model "${model}" already exists at group scope. A model cannot be both group-scoped and tenant-scoped.`);
176
+ const normalizedTenantClasses = tenantClasses.includes("commons")
177
+ ? ["commons"]
178
+ : tenantClasses;
179
+ // Anti-shadowing: reject if a model with the same name exists at a different scope level
180
+ const isGlobal = normalizedTenantClasses.includes("commons") &&
181
+ (!indexes || indexes.length === 0);
182
+ const isTenantScoped = indexes && indexes.length > 0;
183
+ const isGroupScoped = !isGlobal && !isTenantScoped;
184
+ await this.sdk.collection.refresh(this.config.platformIndex, plugin_1.InternalCollection.MODELS);
185
+ const existing = await this.sdk.document.search(this.config.platformIndex, plugin_1.InternalCollection.MODELS, {
186
+ query: {
187
+ bool: {
188
+ must: [
189
+ { term: { type: "asset" } },
190
+ { term: { "asset.model": model } },
191
+ ],
192
+ },
193
+ },
194
+ }, { size: 10 });
195
+ for (const hit of existing.hits) {
196
+ const doc = hit._source;
197
+ const docIsGlobal = doc.tenantClasses?.includes("commons") &&
198
+ (!doc.indexes || doc.indexes.length === 0);
199
+ const docIsTenantScoped = doc.indexes && doc.indexes.length > 0;
200
+ const docIsGroupScoped = !docIsGlobal && !docIsTenantScoped;
201
+ // Same scope level: allow overwrite
202
+ if (isGlobal && docIsGlobal) {
203
+ continue;
204
+ }
205
+ if (isTenantScoped && docIsTenantScoped) {
206
+ continue;
207
+ }
208
+ if (isGroupScoped && docIsGroupScoped) {
209
+ continue;
210
+ }
211
+ // Different scope levels: reject
212
+ if (docIsGlobal) {
213
+ throw new kuzzle_1.BadRequestError(`Asset model "${model}" already exists as a global (commons) model. A model cannot exist at multiple scopes.`);
214
+ }
215
+ if (isGlobal) {
216
+ throw new kuzzle_1.BadRequestError(`Asset model "${model}" already exists at a non-global scope. A model cannot exist at multiple scopes.`);
217
+ }
218
+ if (isTenantScoped && docIsGroupScoped) {
219
+ throw new kuzzle_1.BadRequestError(`Asset model "${model}" already exists at group scope. A model cannot be both group-scoped and tenant-scoped.`);
220
+ }
221
+ if (isGroupScoped && docIsTenantScoped) {
222
+ throw new kuzzle_1.BadRequestError(`Asset model "${model}" already exists at tenant scope. A model cannot be both group-scoped and tenant-scoped.`);
223
+ }
213
224
  }
214
- if (isGroupScoped && docIsTenantScoped) {
215
- throw new kuzzle_1.BadRequestError(`Asset model "${model}" already exists at tenant scope. A model cannot be both group-scoped and tenant-scoped.`);
225
+ const { fields: metadata, resolvedMappings } = await this.resolveMetadata("asset", model, { defaultMetadata, metadataDetails, metadataMappings });
226
+ const modelContent = {
227
+ asset: {
228
+ ...metadata,
229
+ icon,
230
+ locales,
231
+ measures,
232
+ metadataGroups,
233
+ model,
234
+ tooltipModels,
235
+ },
236
+ ...(indexes?.length
237
+ ? { indexes }
238
+ : { tenantClasses: normalizedTenantClasses }),
239
+ type: "asset",
240
+ };
241
+ const modelId = ModelSerializer_1.ModelSerializer.id("asset", modelContent);
242
+ const stored = await this.getStoredModel(modelId);
243
+ this.checkReferentialOnly("asset", model, metadataMappings, stored);
244
+ this.checkDefaultValues(resolvedMappings, metadata.defaultMetadata);
245
+ const conflicts = await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:engine:doesUpdateConflict", { twin: { models: [modelContent], type: "asset" } });
246
+ if (conflicts.length > 0) {
247
+ throw new MappingsConflictsError_1.MappingsConflictsError(`New assets mappings are causing conflicts`, conflicts);
216
248
  }
217
- }
218
- const modelContent = {
219
- asset: {
220
- defaultMetadata,
221
- icon,
222
- locales,
223
- measures,
224
- metadataDetails,
225
- metadataGroups,
226
- metadataMappings,
227
- model,
228
- tooltipModels,
229
- },
230
- ...(indexes?.length
231
- ? { indexes }
232
- : { tenantClasses: normalizedTenantClasses }),
233
- type: "asset",
234
- };
235
- this.checkDefaultValues(metadataMappings, defaultMetadata);
236
- const conflicts = await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:engine:doesUpdateConflict", { twin: { models: [modelContent], type: "asset" } });
237
- if (conflicts.length > 0) {
238
- throw new MappingsConflictsError_1.MappingsConflictsError(`New assets mappings are causing conflicts`, conflicts);
239
- }
240
- const assetModel = await this.sdk.document.createOrReplace(this.config.platformIndex, plugin_1.InternalCollection.MODELS, ModelSerializer_1.ModelSerializer.id("asset", modelContent), modelContent);
241
- await this.sdk.collection.refresh(this.config.platformIndex, plugin_1.InternalCollection.MODELS);
242
- await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:engine:updateAll");
243
- await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:asset:refresh-model", {
244
- assetModel: assetModel._source,
249
+ const assetModel = await this.sdk.document.createOrReplace(this.config.platformIndex, plugin_1.InternalCollection.MODELS, ModelSerializer_1.ModelSerializer.id("asset", modelContent), modelContent);
250
+ await this.sdk.collection.refresh(this.config.platformIndex, plugin_1.InternalCollection.MODELS);
251
+ await this.updateEnginesOrRestore(modelId, stored);
252
+ await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:asset:refresh-model", {
253
+ assetModel: assetModel._source,
254
+ });
255
+ return assetModel;
245
256
  });
246
- return assetModel;
247
257
  }
248
258
  async writeDevice(model, metadataMappings, defaultMetadata, metadataDetails, metadataGroups, measures, icon) {
249
- if (kuzzle_1.Inflector.pascalCase(model) !== model) {
250
- throw new kuzzle_1.BadRequestError(`Device model "${model}" must be PascalCase.`);
251
- }
252
- const duplicates = (0, MeasuresDuplicates_1.getNamedMeasuresDuplicates)(measures);
253
- if (duplicates.length > 0) {
254
- throw new MeasuresNamesDuplicatesError_1.MeasuresNamesDuplicatesError("Device model measures contain one or multiple duplicate measure name", duplicates);
255
- }
256
- const modelContent = {
257
- device: {
259
+ return (0, MetadataReferential_1.withMetadataReferentialLock)(async () => {
260
+ if (kuzzle_1.Inflector.pascalCase(model) !== model) {
261
+ throw new kuzzle_1.BadRequestError(`Device model "${model}" must be PascalCase.`);
262
+ }
263
+ const duplicates = (0, MeasuresDuplicates_1.getNamedMeasuresDuplicates)(measures);
264
+ if (duplicates.length > 0) {
265
+ throw new MeasuresNamesDuplicatesError_1.MeasuresNamesDuplicatesError("Device model measures contain one or multiple duplicate measure name", duplicates);
266
+ }
267
+ const { fields: metadata } = await this.resolveMetadata("device", model, {
258
268
  defaultMetadata,
259
- icon,
260
- measures,
261
269
  metadataDetails,
262
- metadataGroups,
263
270
  metadataMappings,
264
- model,
265
- },
266
- type: "device",
267
- };
268
- const conflicts = await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:engine:doesUpdateConflict", { twin: { models: [modelContent], type: "device" } });
269
- if (conflicts.length > 0) {
270
- throw new MappingsConflictsError_1.MappingsConflictsError(`New devices mappings are causing conflicts`, conflicts);
271
- }
272
- const deviceModel = await this.sdk.document.createOrReplace(this.config.platformIndex, plugin_1.InternalCollection.MODELS, ModelSerializer_1.ModelSerializer.id("device", modelContent), modelContent);
273
- await this.sdk.collection.refresh(this.config.platformIndex, plugin_1.InternalCollection.MODELS);
274
- await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:engine:updateAll");
275
- await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:device:refresh-model", {
276
- deviceModel: modelContent,
271
+ });
272
+ const modelContent = {
273
+ device: {
274
+ ...metadata,
275
+ icon,
276
+ measures,
277
+ metadataGroups,
278
+ model,
279
+ },
280
+ type: "device",
281
+ };
282
+ const modelId = ModelSerializer_1.ModelSerializer.id("device", modelContent);
283
+ const stored = await this.getStoredModel(modelId);
284
+ this.checkReferentialOnly("device", model, metadataMappings, stored);
285
+ const conflicts = await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:engine:doesUpdateConflict", { twin: { models: [modelContent], type: "device" } });
286
+ if (conflicts.length > 0) {
287
+ throw new MappingsConflictsError_1.MappingsConflictsError(`New devices mappings are causing conflicts`, conflicts);
288
+ }
289
+ const deviceModel = await this.sdk.document.createOrReplace(this.config.platformIndex, plugin_1.InternalCollection.MODELS, ModelSerializer_1.ModelSerializer.id("device", modelContent), modelContent);
290
+ await this.sdk.collection.refresh(this.config.platformIndex, plugin_1.InternalCollection.MODELS);
291
+ await this.updateEnginesOrRestore(modelId, stored);
292
+ await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:device:refresh-model", {
293
+ deviceModel: modelContent,
294
+ });
295
+ return deviceModel;
277
296
  });
278
- return deviceModel;
279
297
  }
280
298
  async writeGroup(tenantClasses, model, affinity, metadataMappings, defaultMetadata, metadataDetails, metadataGroups, icon, locales) {
281
- if (kuzzle_1.Inflector.pascalCase(model) !== model) {
282
- throw new kuzzle_1.BadRequestError(`Group model "${model}" must be PascalCase.`);
283
- }
284
- if (tenantClasses.includes("commons")) {
285
- tenantClasses = ["commons"];
286
- }
287
- const groupAffinity = this.checkGroupAffinity(affinity);
288
- const modelContent = {
289
- tenantClasses,
290
- group: {
291
- affinity: groupAffinity,
299
+ return (0, MetadataReferential_1.withMetadataReferentialLock)(async () => {
300
+ if (kuzzle_1.Inflector.pascalCase(model) !== model) {
301
+ throw new kuzzle_1.BadRequestError(`Group model "${model}" must be PascalCase.`);
302
+ }
303
+ if (tenantClasses.includes("commons")) {
304
+ tenantClasses = ["commons"];
305
+ }
306
+ const groupAffinity = this.checkGroupAffinity(affinity);
307
+ const { fields: metadata } = await this.resolveMetadata("group", model, {
292
308
  defaultMetadata,
293
- icon,
294
- locales,
295
309
  metadataDetails,
296
- metadataGroups,
297
310
  metadataMappings,
298
- model,
299
- },
300
- type: "group",
301
- };
302
- const conflicts = await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:engine:doesUpdateConflict", { groupModels: [modelContent] });
303
- if (conflicts.length > 0) {
304
- throw new MappingsConflictsError_1.MappingsConflictsError(`New group mappings are causing conflicts`, conflicts);
305
- }
306
- const groupModel = await this.sdk.document.createOrReplace(this.config.platformIndex, plugin_1.InternalCollection.MODELS, ModelSerializer_1.ModelSerializer.id("group", modelContent), modelContent);
307
- await this.sdk.collection.refresh(this.config.platformIndex, plugin_1.InternalCollection.MODELS);
308
- await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:engine:updateAll");
309
- return groupModel;
311
+ });
312
+ const modelContent = {
313
+ tenantClasses,
314
+ group: {
315
+ ...metadata,
316
+ affinity: groupAffinity,
317
+ icon,
318
+ locales,
319
+ metadataGroups,
320
+ model,
321
+ },
322
+ type: "group",
323
+ };
324
+ const modelId = ModelSerializer_1.ModelSerializer.id("group", modelContent);
325
+ const stored = await this.getStoredModel(modelId);
326
+ this.checkReferentialOnly("group", model, metadataMappings, stored);
327
+ const conflicts = await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:engine:doesUpdateConflict", { groupModels: [modelContent] });
328
+ if (conflicts.length > 0) {
329
+ throw new MappingsConflictsError_1.MappingsConflictsError(`New group mappings are causing conflicts`, conflicts);
330
+ }
331
+ const groupModel = await this.sdk.document.createOrReplace(this.config.platformIndex, plugin_1.InternalCollection.MODELS, ModelSerializer_1.ModelSerializer.id("group", modelContent), modelContent);
332
+ await this.sdk.collection.refresh(this.config.platformIndex, plugin_1.InternalCollection.MODELS);
333
+ await this.updateEnginesOrRestore(modelId, stored);
334
+ return groupModel;
335
+ });
310
336
  }
311
337
  async writeMeasure(type, valuesMappings, validationSchema, valuesDetails, locales, indexes, icon) {
312
338
  const modelContent = {
@@ -337,9 +363,11 @@ class ModelService extends shared_1.BaseService {
337
363
  if (conflicts.length > 0) {
338
364
  throw new MappingsConflictsError_1.MappingsConflictsError(`New assets mappings are causing conflicts`, conflicts);
339
365
  }
340
- const measureModel = await this.sdk.document.createOrReplace(this.config.platformIndex, plugin_1.InternalCollection.MODELS, ModelSerializer_1.ModelSerializer.id("measure", modelContent), modelContent);
366
+ const modelId = ModelSerializer_1.ModelSerializer.id("measure", modelContent);
367
+ const stored = await this.getStoredModel(modelId);
368
+ const measureModel = await this.sdk.document.createOrReplace(this.config.platformIndex, plugin_1.InternalCollection.MODELS, modelId, modelContent);
341
369
  await this.sdk.collection.refresh(this.config.platformIndex, plugin_1.InternalCollection.MODELS);
342
- await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:engine:updateAll");
370
+ await this.updateEnginesOrRestore(modelId, stored);
343
371
  return measureModel;
344
372
  }
345
373
  async deleteAsset(_id) {
@@ -737,60 +765,333 @@ class ModelService extends shared_1.BaseService {
737
765
  * Update an asset model
738
766
  */
739
767
  async updateAsset(tenantClasses, index, model, metadataMappings, defaultMetadata, metadataDetails, metadataGroups, measures, tooltipModels, locales, icon, request) {
740
- if (kuzzle_1.Inflector.pascalCase(model) !== model) {
741
- throw new kuzzle_1.BadRequestError(`Asset model "${model}" must be PascalCase.`);
742
- }
743
- this.checkDefaultValues(metadataMappings, defaultMetadata);
744
- const existingAsset = await this.getAsset(tenantClasses, index, model);
745
- // The field must be deleted if an element of the table is to be deleted
746
- await this.sdk.document.deleteFields(this.config.platformIndex, plugin_1.InternalCollection.MODELS, existingAsset._id, [
747
- "asset.tooltipModels",
748
- "asset.metadataMappings",
749
- "asset.defaultMetadata",
750
- "asset.metadataDetails",
751
- "asset.metadataGroups",
752
- ], { source: true });
753
- const measuresUpdated = measures.length === 0 ? existingAsset._source.asset.measures : measures;
754
- // Preserve the existing scope: scope (tenantClasses / indexes) is decided
755
- // at write time and must not be silently rewritten by an update payload.
756
- // This also enforces tenantClasses / indexes mutual exclusivity (KZLPRD-1192).
757
- const assetModelContent = {
758
- asset: {
759
- defaultMetadata,
760
- icon: icon ?? existingAsset._source.asset.icon,
761
- locales,
762
- measures: measuresUpdated,
763
- metadataDetails,
764
- metadataGroups,
768
+ return (0, MetadataReferential_1.withMetadataReferentialLock)(async () => {
769
+ if (kuzzle_1.Inflector.pascalCase(model) !== model) {
770
+ throw new kuzzle_1.BadRequestError(`Asset model "${model}" must be PascalCase.`);
771
+ }
772
+ const existingAsset = await this.getAsset(tenantClasses, index, model);
773
+ this.checkReferentialOnly("asset", model, metadataMappings, existingAsset._source);
774
+ const { fields: metadata, resolvedMappings } = await this.resolveMetadata("asset", model, { defaultMetadata, metadataDetails, metadataMappings });
775
+ this.checkDefaultValues(resolvedMappings, metadata.defaultMetadata);
776
+ const measuresUpdated = measures.length === 0 ? existingAsset._source.asset.measures : measures;
777
+ // Preserve the existing scope: scope (tenantClasses / indexes) is decided
778
+ // at write time and must not be silently rewritten by an update payload.
779
+ // This also enforces tenantClasses / indexes mutual exclusivity (KZLPRD-1192).
780
+ const assetModelContent = {
781
+ asset: {
782
+ ...metadata,
783
+ icon: icon ?? existingAsset._source.asset.icon,
784
+ locales,
785
+ measures: measuresUpdated,
786
+ metadataGroups,
787
+ model,
788
+ tooltipModels,
789
+ },
790
+ tenantClasses: existingAsset._source.tenantClasses,
791
+ indexes: existingAsset._source.indexes,
792
+ type: "asset",
793
+ };
794
+ const assetModel = {
795
+ _id: existingAsset._id,
796
+ _source: assetModelContent,
797
+ };
798
+ const conflicts = await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:engine:doesUpdateConflict", { twin: { models: [assetModelContent], type: "asset" } });
799
+ if (conflicts.length > 0) {
800
+ throw new MappingsConflictsError_1.MappingsConflictsError(`Assets mappings are causing conflicts`, conflicts);
801
+ }
802
+ // The field must be deleted if an element of the table is to be deleted.
803
+ // ? Only after the conflicts check, so a rejected update leaves the model untouched
804
+ await this.sdk.document.deleteFields(this.config.platformIndex, plugin_1.InternalCollection.MODELS, existingAsset._id, [
805
+ "asset.tooltipModels",
806
+ "asset.metadataMappings",
807
+ "asset.defaultMetadata",
808
+ "asset.metadataDetails",
809
+ "asset.metadataGroups",
810
+ ], { source: true });
811
+ const endDocument = await this.updateDocument(request, assetModel, {
812
+ collection: plugin_1.InternalCollection.MODELS,
813
+ index: this.config.platformIndex,
814
+ }, { source: true });
815
+ // ? Only update indexes and refresh asset models when necessary
816
+ if (Object.keys(metadata.metadataMappings).length > 0 ||
817
+ measures.length > 0) {
818
+ await this.sdk.collection.refresh(this.config.platformIndex, plugin_1.InternalCollection.MODELS);
819
+ await this.updateEnginesOrRestore(existingAsset._id, existingAsset._source);
820
+ await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:asset:refresh-model", {
821
+ assetModel: endDocument._source,
822
+ });
823
+ }
824
+ return endDocument;
825
+ });
826
+ }
827
+ /**
828
+ * Adds metadata of the metadata referential to a device model.
829
+ *
830
+ * Device models are additive only from the API: every existing metadata must be kept unchanged,
831
+ * new metadata must come from the referential. Metadata details and default values can change.
832
+ * The engines mappings are updated and the devices get the new metadata with their default value.
833
+ *
834
+ * @throws BadRequestError if an existing metadata is missing or changed, or a new one is inline
835
+ * @throws MappingsConflictsError if the new metadata conflict with the existing mappings
836
+ */
837
+ async updateDevice(model, { defaultMetadata, metadataDetails, metadataMappings }) {
838
+ return (0, MetadataReferential_1.withMetadataReferentialLock)(async () => {
839
+ const existingDevice = await this.getDevice(model);
840
+ const existing = existingDevice._source.device;
841
+ const existingMappings = existing.metadataMappings ?? {};
842
+ const removed = Object.keys(existingMappings).filter((name) => !lodash_1.default.isEqual(existingMappings[name], metadataMappings[name]));
843
+ if (removed.length > 0) {
844
+ throw new kuzzle_1.BadRequestError(`Device model "${model}" metadata cannot be removed or modified: ${removed.join(", ")}`);
845
+ }
846
+ const inline = Object.keys(metadataMappings).filter((name) => !(name in existingMappings) &&
847
+ !(0, MetadataReferential_1.isReferentialMapping)(metadataMappings[name]));
848
+ if (inline.length > 0) {
849
+ throw new kuzzle_1.BadRequestError(`New metadata of device model "${model}" must come from the metadata referential, inline metadata: ${inline.join(", ")}`);
850
+ }
851
+ const { fields: metadata } = await this.resolveMetadata("device", model, {
852
+ defaultMetadata: defaultMetadata ?? existing.defaultMetadata,
853
+ metadataDetails: metadataDetails ?? existing.metadataDetails,
765
854
  metadataMappings,
766
- model,
767
- tooltipModels,
855
+ });
856
+ const deviceModelContent = {
857
+ ...existingDevice._source,
858
+ device: { ...existing, ...metadata },
859
+ };
860
+ const conflicts = await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:engine:doesUpdateConflict", { twin: { models: [deviceModelContent], type: "device" } });
861
+ if (conflicts.length > 0) {
862
+ throw new MappingsConflictsError_1.MappingsConflictsError(`Devices mappings are causing conflicts`, conflicts);
863
+ }
864
+ const deviceModel = await this.sdk.document.createOrReplace(this.config.platformIndex, plugin_1.InternalCollection.MODELS, existingDevice._id, deviceModelContent);
865
+ await this.sdk.collection.refresh(this.config.platformIndex, plugin_1.InternalCollection.MODELS);
866
+ await this.updateEnginesOrRestore(existingDevice._id, existingDevice._source);
867
+ await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:device:refresh-model", { deviceModel: deviceModel._source });
868
+ return deviceModel;
869
+ });
870
+ }
871
+ /**
872
+ * Returns the group model available for the given tenant classes:
873
+ * a model of these classes first, then a commons model.
874
+ *
875
+ * @throws NotFoundError if no group model with this name is available
876
+ */
877
+ async getScopedGroup(tenantClasses, model) {
878
+ const result = await this.sdk.document.search(this.config.platformIndex, plugin_1.InternalCollection.MODELS, {
879
+ query: {
880
+ and: [
881
+ { equals: { type: "group" } },
882
+ { equals: { "group.model": model } },
883
+ ],
768
884
  },
769
- tenantClasses: existingAsset._source.tenantClasses,
770
- indexes: existingAsset._source.indexes,
771
- type: "asset",
772
- };
773
- const assetModel = {
774
- _id: existingAsset._id,
775
- _source: assetModelContent,
776
- };
777
- const conflicts = await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:engine:doesUpdateConflict", { twin: { models: [assetModelContent], type: "asset" } });
778
- if (conflicts.length > 0) {
779
- throw new MappingsConflictsError_1.MappingsConflictsError(`Assets mappings are causing conflicts`, conflicts);
780
- }
781
- const endDocument = await this.updateDocument(request, assetModel, {
782
- collection: plugin_1.InternalCollection.MODELS,
783
- index: this.config.platformIndex,
784
- }, { source: true });
785
- // ? Only update indexes and refresh asset models when necessary
786
- if (Object.keys(metadataMappings).length > 0 || measures.length > 0) {
885
+ }, { lang: "koncorde", size: 100 });
886
+ const scoped = result.hits.find(({ _source }) => _source.tenantClasses.some((tenantClass) => tenantClass !== "commons" && tenantClasses.includes(tenantClass)));
887
+ const common = result.hits.find(({ _source }) => _source.tenantClasses.includes("commons"));
888
+ const groupModel = scoped ?? common;
889
+ if (!groupModel) {
890
+ throw new kuzzle_1.NotFoundError(`Unknown Group model "${model}" for tenant classes "${tenantClasses.join(", ")}".`);
891
+ }
892
+ return { _id: groupModel._id, _source: groupModel._source };
893
+ }
894
+ /**
895
+ * Updates a group model, keeping its scope.
896
+ * Affinity and icon are kept when omitted.
897
+ *
898
+ * The engines mappings are updated and the groups of the model are refreshed.
899
+ *
900
+ * @throws MappingsConflictsError if the new mappings conflict with the existing ones
901
+ */
902
+ async updateGroup(tenantClasses, model, { affinity, defaultMetadata, icon, locales, metadataDetails, metadataGroups, metadataMappings, }) {
903
+ return (0, MetadataReferential_1.withMetadataReferentialLock)(async () => {
904
+ if (kuzzle_1.Inflector.pascalCase(model) !== model) {
905
+ throw new kuzzle_1.BadRequestError(`Group model "${model}" must be PascalCase.`);
906
+ }
907
+ const existingGroup = await this.getScopedGroup(tenantClasses, model);
908
+ const existing = existingGroup._source.group;
909
+ this.checkReferentialOnly("group", model, metadataMappings, existingGroup._source);
910
+ const { fields: metadata, resolvedMappings } = await this.resolveMetadata("group", model, { defaultMetadata, metadataDetails, metadataMappings });
911
+ this.checkDefaultValues(resolvedMappings, metadata.defaultMetadata);
912
+ const groupModelContent = {
913
+ tenantClasses: existingGroup._source.tenantClasses,
914
+ group: {
915
+ ...metadata,
916
+ affinity: affinity === undefined
917
+ ? existing.affinity
918
+ : this.checkGroupAffinity(affinity),
919
+ icon: icon ?? existing.icon,
920
+ locales,
921
+ metadataGroups,
922
+ model,
923
+ },
924
+ type: "group",
925
+ };
926
+ const conflicts = await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:engine:doesUpdateConflict", { groupModels: [groupModelContent] });
927
+ if (conflicts.length > 0) {
928
+ throw new MappingsConflictsError_1.MappingsConflictsError(`Group mappings are causing conflicts`, conflicts);
929
+ }
930
+ const groupModel = await this.sdk.document.createOrReplace(this.config.platformIndex, plugin_1.InternalCollection.MODELS, existingGroup._id, groupModelContent);
787
931
  await this.sdk.collection.refresh(this.config.platformIndex, plugin_1.InternalCollection.MODELS);
788
- await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:engine:updateAll");
789
- await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:asset:refresh-model", {
790
- assetModel: endDocument._source,
932
+ await this.updateEnginesOrRestore(existingGroup._id, existingGroup._source);
933
+ await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:group:refresh-model", {
934
+ groupModel: groupModel._source,
791
935
  });
936
+ return groupModel;
937
+ });
938
+ }
939
+ /**
940
+ * Validates the metadata of a model written through the API against the metadata referential
941
+ * and enriches the details and default values of the metadata coming from it
942
+ */
943
+ async resolveMetadata(modelType, model, fields) {
944
+ const enrichment = (0, MetadataReferential_1.enrichModelMetadata)(await this.getMetadataReferential(), fields, {
945
+ createError: (message) => new kuzzle_1.BadRequestError(message),
946
+ model,
947
+ modelType,
948
+ });
949
+ for (const warning of enrichment.warnings) {
950
+ this.logger.warn(warning);
951
+ }
952
+ return enrichment;
953
+ }
954
+ /**
955
+ * Resolves the referential metadata of stored models, to be returned by the API
956
+ */
957
+ async resolveModels(documents) {
958
+ const referential = await this.getMetadataReferential();
959
+ return documents.map((document) => ({
960
+ ...document,
961
+ _source: (0, MetadataReferential_1.resolveModelMetadata)(referential, document._source),
962
+ }));
963
+ }
964
+ async getMetadataReferential() {
965
+ return (0, MetadataReferential_1.fetchMetadataReferential)(this.sdk, this.config.platformIndex);
966
+ }
967
+ /**
968
+ * Creates a metadata of the metadata referential, or updates its details.
969
+ *
970
+ * The mappings of a new metadata are merged into the assets, devices and groups collections
971
+ * of every engine. They cannot be modified afterwards: only the locales, editor hint,
972
+ * default value and icon of an existing metadata can change. These are copied into the models
973
+ * using the metadata when they are written, the existing models are not modified.
974
+ *
975
+ * @throws BadRequestError if the mappings of an existing metadata are modified
976
+ * @throws MappingsConflictsError if the new metadata conflicts with an inline metadata of a model
977
+ * or with the actual mappings of an engine collection
978
+ */
979
+ async writeMetadata(name, definition) {
980
+ (0, MetadataReferential_1.checkMetadataReferentialEntry)(name, definition, (message) => new kuzzle_1.BadRequestError(message));
981
+ return (0, MetadataReferential_1.withMetadataReferentialLock)(async () => {
982
+ const stored = await this.getMetadataReferential();
983
+ const entry = {
984
+ locales: {},
985
+ ...lodash_1.default.omit(definition, "managed"),
986
+ };
987
+ this.checkUnmanagedMetadata(stored, name);
988
+ if (stored[name]) {
989
+ if (!lodash_1.default.isEqual(stored[name].mappings, entry.mappings)) {
990
+ throw new kuzzle_1.BadRequestError(`The mappings of metadata "${name}" cannot be modified, they are already merged into the engines collections`);
991
+ }
992
+ const referential = { ...stored, [name]: entry };
993
+ await (0, MetadataReferential_1.saveMetadataReferential)(this.sdk, this.config.platformIndex, referential);
994
+ return referential;
995
+ }
996
+ const models = await (0, MetadataReferential_1.fetchMetadataModels)(this.sdk, this.config.platformIndex);
997
+ const modelsConflicts = (0, MetadataReferential_1.getEntryModelsConflicts)(name, entry, models.map(({ _source }) => _source));
998
+ if (modelsConflicts.length > 0) {
999
+ throw new MappingsConflictsError_1.MappingsConflictsError(`Metadata "${name}" conflicts with the metadata of models`, modelsConflicts);
1000
+ }
1001
+ const esConflicts = await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:engine:doesMetadataConflict", { metadataMappings: { [name]: entry.mappings } });
1002
+ if (esConflicts.length > 0) {
1003
+ throw new MappingsConflictsError_1.MappingsConflictsError(`Metadata "${name}" conflicts with the engines collections mappings`, esConflicts);
1004
+ }
1005
+ const referential = { ...stored, [name]: entry };
1006
+ await (0, MetadataReferential_1.saveMetadataReferential)(this.sdk, this.config.platformIndex, referential);
1007
+ try {
1008
+ await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:engine:updateAll");
1009
+ }
1010
+ catch (error) {
1011
+ // ? A metadata rejected by Elasticsearch would make every next engines update fail
1012
+ await (0, MetadataReferential_1.saveMetadataReferential)(this.sdk, this.config.platformIndex, stored);
1013
+ throw error;
1014
+ }
1015
+ return referential;
1016
+ });
1017
+ }
1018
+ /**
1019
+ * Deletes a metadata of the metadata referential.
1020
+ * Its mappings stay in the engines collections, Elasticsearch cannot remove them.
1021
+ *
1022
+ * @throws BadRequestError if a model still uses the metadata
1023
+ */
1024
+ async deleteMetadata(name) {
1025
+ return (0, MetadataReferential_1.withMetadataReferentialLock)(async () => {
1026
+ const referential = await this.getMetadataReferential();
1027
+ if (!referential[name]) {
1028
+ throw new kuzzle_1.NotFoundError(`Unknown metadata "${name}" in the metadata referential`);
1029
+ }
1030
+ this.checkUnmanagedMetadata(referential, name);
1031
+ const referencingModels = await (0, MetadataReferential_1.fetchReferencingModels)(this.sdk, this.config.platformIndex, name);
1032
+ if (referencingModels.length > 0) {
1033
+ throw new kuzzle_1.BadRequestError(`Metadata "${name}" is still used by the models: ${referencingModels
1034
+ .map(({ _id }) => _id)
1035
+ .join(", ")}`);
1036
+ }
1037
+ const updatedReferential = lodash_1.default.omit(referential, name);
1038
+ await (0, MetadataReferential_1.saveMetadataReferential)(this.sdk, this.config.platformIndex, updatedReferential);
1039
+ return updatedReferential;
1040
+ });
1041
+ }
1042
+ /**
1043
+ * With the referentialOnly option, rejects the inline metadata a model write adds.
1044
+ * The inline metadata already defined by the stored model are kept (legacy).
1045
+ *
1046
+ * @throws BadRequestError if new inline metadata are added
1047
+ */
1048
+ checkReferentialOnly(modelType, model, metadataMappings, stored) {
1049
+ if (!this.config.models?.metadata?.referentialOnly) {
1050
+ return;
1051
+ }
1052
+ const added = (0, MetadataReferential_1.getAddedInlineMetadata)(metadataMappings, stored);
1053
+ if (added.length > 0) {
1054
+ throw new kuzzle_1.BadRequestError(`New metadata of ${modelType} model "${model}" must come from the metadata referential, inline metadata: ${added.join(", ")}`);
1055
+ }
1056
+ }
1057
+ /**
1058
+ * Updates the engines collections. If Elasticsearch rejects the new mappings, the model is
1059
+ * restored to its previous version (or deleted if it is new), otherwise every next engines
1060
+ * update would fail on it.
1061
+ */
1062
+ async updateEnginesOrRestore(_id, previous) {
1063
+ try {
1064
+ await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:engine:updateAll");
1065
+ }
1066
+ catch (error) {
1067
+ if (previous) {
1068
+ await this.sdk.document.createOrReplace(this.config.platformIndex, plugin_1.InternalCollection.MODELS, _id, lodash_1.default.omit(previous, "_kuzzle_info"), { refresh: "wait_for" });
1069
+ }
1070
+ else {
1071
+ await this.sdk.document.delete(this.config.platformIndex, plugin_1.InternalCollection.MODELS, _id, { refresh: "wait_for" });
1072
+ }
1073
+ throw error;
1074
+ }
1075
+ }
1076
+ async getStoredModel(_id) {
1077
+ try {
1078
+ const { _source } = await this.sdk.document.get(this.config.platformIndex, plugin_1.InternalCollection.MODELS, _id);
1079
+ return _source;
1080
+ }
1081
+ catch (error) {
1082
+ if (error.status === 404) {
1083
+ return null;
1084
+ }
1085
+ throw error;
1086
+ }
1087
+ }
1088
+ /**
1089
+ * @throws BadRequestError if the metadata is registered from code
1090
+ */
1091
+ checkUnmanagedMetadata(referential, name) {
1092
+ if (referential[name]?.managed) {
1093
+ throw new kuzzle_1.BadRequestError(`Metadata "${name}" is registered from code, it cannot be modified or deleted through the API`);
792
1094
  }
793
- return endDocument;
794
1095
  }
795
1096
  }
796
1097
  exports.ModelService = ModelService;