kuzzle-device-manager 3.0.0-3-dev.19 → 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 (118) hide show
  1. package/dist/bin/migration/migrate-tenant-classes.d.ts +2 -0
  2. package/dist/bin/migration/migrate-tenant-classes.js +133 -0
  3. package/dist/bin/migration/migrate-tenant-classes.js.map +1 -0
  4. package/dist/lib/modules/asset/AssetHistoryService.d.ts +1 -1
  5. package/dist/lib/modules/asset/AssetHistoryService.js +3 -3
  6. package/dist/lib/modules/asset/AssetHistoryService.js.map +1 -1
  7. package/dist/lib/modules/asset/AssetService.d.ts +18 -12
  8. package/dist/lib/modules/asset/AssetService.js +115 -115
  9. package/dist/lib/modules/asset/AssetService.js.map +1 -1
  10. package/dist/lib/modules/asset/AssetsController.d.ts +1 -1
  11. package/dist/lib/modules/asset/AssetsController.js +72 -74
  12. package/dist/lib/modules/asset/AssetsController.js.map +1 -1
  13. package/dist/lib/modules/asset/roles/RoleAssetsAdmin.d.ts +1 -0
  14. package/dist/lib/modules/asset/roles/RoleAssetsAdmin.js +2 -0
  15. package/dist/lib/modules/asset/roles/RoleAssetsAdmin.js.map +1 -1
  16. package/dist/lib/modules/asset/roles/RoleAssetsReader.d.ts +1 -0
  17. package/dist/lib/modules/asset/roles/RoleAssetsReader.js +2 -0
  18. package/dist/lib/modules/asset/roles/RoleAssetsReader.js.map +1 -1
  19. package/dist/lib/modules/asset/types/AssetApi.d.ts +6 -6
  20. package/dist/lib/modules/asset/types/AssetEvents.d.ts +1 -1
  21. package/dist/lib/modules/decoder/PayloadService.js +16 -16
  22. package/dist/lib/modules/decoder/PayloadService.js.map +1 -1
  23. package/dist/lib/modules/device/DeviceService.d.ts +11 -11
  24. package/dist/lib/modules/device/DeviceService.js +86 -86
  25. package/dist/lib/modules/device/DeviceService.js.map +1 -1
  26. package/dist/lib/modules/device/DevicesController.js +60 -64
  27. package/dist/lib/modules/device/DevicesController.js.map +1 -1
  28. package/dist/lib/modules/device/collections/deviceMappings.js +2 -2
  29. package/dist/lib/modules/device/collections/deviceMappings.js.map +1 -1
  30. package/dist/lib/modules/device/roles/RoleDevicesAdmin.d.ts +1 -0
  31. package/dist/lib/modules/device/roles/RoleDevicesAdmin.js +2 -0
  32. package/dist/lib/modules/device/roles/RoleDevicesAdmin.js.map +1 -1
  33. package/dist/lib/modules/device/roles/RoleDevicesReader.d.ts +1 -0
  34. package/dist/lib/modules/device/roles/RoleDevicesReader.js +2 -0
  35. package/dist/lib/modules/device/roles/RoleDevicesReader.js.map +1 -1
  36. package/dist/lib/modules/device/types/DeviceApi.d.ts +2 -2
  37. package/dist/lib/modules/device/types/DeviceContent.d.ts +2 -2
  38. package/dist/lib/modules/device/types/DeviceEvents.d.ts +1 -1
  39. package/dist/lib/modules/group/GroupsController.d.ts +3 -3
  40. package/dist/lib/modules/group/GroupsController.js +66 -66
  41. package/dist/lib/modules/group/GroupsController.js.map +1 -1
  42. package/dist/lib/modules/group/GroupsService.d.ts +19 -14
  43. package/dist/lib/modules/group/GroupsService.js +103 -64
  44. package/dist/lib/modules/group/GroupsService.js.map +1 -1
  45. package/dist/lib/modules/group/index.d.ts +1 -0
  46. package/dist/lib/modules/group/index.js +1 -0
  47. package/dist/lib/modules/group/index.js.map +1 -1
  48. package/dist/lib/modules/group/roles/RoleGroupsAdmin.d.ts +1 -0
  49. package/dist/lib/modules/group/roles/RoleGroupsAdmin.js +2 -0
  50. package/dist/lib/modules/group/roles/RoleGroupsAdmin.js.map +1 -1
  51. package/dist/lib/modules/group/roles/RoleGroupsReader.d.ts +1 -0
  52. package/dist/lib/modules/group/roles/RoleGroupsReader.js +2 -0
  53. package/dist/lib/modules/group/roles/RoleGroupsReader.js.map +1 -1
  54. package/dist/lib/modules/group/types/GroupEvents.d.ts +8 -0
  55. package/dist/lib/modules/group/types/GroupEvents.js +3 -0
  56. package/dist/lib/modules/group/types/GroupEvents.js.map +1 -0
  57. package/dist/lib/modules/group/types/GroupsApi.d.ts +1 -1
  58. package/dist/lib/modules/measure/MeasureExporter.d.ts +5 -5
  59. package/dist/lib/modules/measure/MeasureExporter.js +20 -20
  60. package/dist/lib/modules/measure/MeasureExporter.js.map +1 -1
  61. package/dist/lib/modules/measure/MeasureService.d.ts +2 -2
  62. package/dist/lib/modules/measure/MeasureService.js +5 -5
  63. package/dist/lib/modules/measure/MeasureService.js.map +1 -1
  64. package/dist/lib/modules/measure/MeasureTargetBuilder.d.ts +1 -1
  65. package/dist/lib/modules/measure/MeasureTargetBuilder.js +2 -2
  66. package/dist/lib/modules/measure/MeasureTargetBuilder.js.map +1 -1
  67. package/dist/lib/modules/measure/types/MeasureTarget.d.ts +1 -1
  68. package/dist/lib/modules/measure/types/MeasureTarget.js +1 -1
  69. package/dist/lib/modules/measure/types/MeasureTarget.js.map +1 -1
  70. package/dist/lib/modules/model/MetadataReferential.d.ts +119 -0
  71. package/dist/lib/modules/model/MetadataReferential.js +342 -0
  72. package/dist/lib/modules/model/MetadataReferential.js.map +1 -0
  73. package/dist/lib/modules/model/ModelSerializer.js +13 -13
  74. package/dist/lib/modules/model/ModelSerializer.js.map +1 -1
  75. package/dist/lib/modules/model/ModelService.d.ts +120 -22
  76. package/dist/lib/modules/model/ModelService.js +548 -263
  77. package/dist/lib/modules/model/ModelService.js.map +1 -1
  78. package/dist/lib/modules/model/ModelsConflicts.d.ts +23 -0
  79. package/dist/lib/modules/model/ModelsConflicts.js +54 -4
  80. package/dist/lib/modules/model/ModelsConflicts.js.map +1 -1
  81. package/dist/lib/modules/model/ModelsController.d.ts +11 -2
  82. package/dist/lib/modules/model/ModelsController.js +151 -54
  83. package/dist/lib/modules/model/ModelsController.js.map +1 -1
  84. package/dist/lib/modules/model/ModelsRegister.d.ts +37 -5
  85. package/dist/lib/modules/model/ModelsRegister.js +107 -9
  86. package/dist/lib/modules/model/ModelsRegister.js.map +1 -1
  87. package/dist/lib/modules/model/collections/modelsMappings.js +15 -1
  88. package/dist/lib/modules/model/collections/modelsMappings.js.map +1 -1
  89. package/dist/lib/modules/model/types/ModelApi.d.ts +105 -14
  90. package/dist/lib/modules/model/types/ModelContent.d.ts +73 -7
  91. package/dist/lib/modules/model/types/ModelContent.js.map +1 -1
  92. package/dist/lib/modules/model/types/ModelDefinition.d.ts +6 -3
  93. package/dist/lib/modules/model/types/ModelEvents.d.ts +9 -4
  94. package/dist/lib/modules/plugin/DeviceManagerEngine.d.ts +58 -25
  95. package/dist/lib/modules/plugin/DeviceManagerEngine.js +184 -88
  96. package/dist/lib/modules/plugin/DeviceManagerEngine.js.map +1 -1
  97. package/dist/lib/modules/plugin/DeviceManagerPlugin.d.ts +46 -4
  98. package/dist/lib/modules/plugin/DeviceManagerPlugin.js +72 -9
  99. package/dist/lib/modules/plugin/DeviceManagerPlugin.js.map +1 -1
  100. package/dist/lib/modules/plugin/types/DeviceManagerConfiguration.d.ts +11 -0
  101. package/dist/lib/modules/shared/services/AbstractExporter.d.ts +8 -8
  102. package/dist/lib/modules/shared/services/AbstractExporter.js +9 -9
  103. package/dist/lib/modules/shared/services/AbstractExporter.js.map +1 -1
  104. package/dist/lib/modules/shared/services/BaseService.d.ts +7 -7
  105. package/dist/lib/modules/shared/services/BaseService.js +20 -17
  106. package/dist/lib/modules/shared/services/BaseService.js.map +1 -1
  107. package/dist/lib/modules/shared/services/DigitalTwinExporter.d.ts +3 -3
  108. package/dist/lib/modules/shared/services/DigitalTwinExporter.js +18 -18
  109. package/dist/lib/modules/shared/services/DigitalTwinExporter.js.map +1 -1
  110. package/dist/lib/modules/shared/services/DigitalTwinService.d.ts +8 -8
  111. package/dist/lib/modules/shared/services/DigitalTwinService.js +39 -39
  112. package/dist/lib/modules/shared/services/DigitalTwinService.js.map +1 -1
  113. package/dist/lib/modules/shared/types/DigitalTwinApi.d.ts +1 -1
  114. package/dist/lib/modules/shared/types/DigitalTwinEvents.d.ts +2 -2
  115. package/dist/lib/modules/shared/utils/lock.d.ts +7 -1
  116. package/dist/lib/modules/shared/utils/lock.js +2 -2
  117. package/dist/lib/modules/shared/utils/lock.js.map +1 -1
  118. package/package.json +3 -2
@@ -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);
@@ -18,8 +23,8 @@ class ModelService extends shared_1.BaseService {
18
23
  this.registerAskEvents();
19
24
  }
20
25
  registerAskEvents() {
21
- (0, kuzzle_plugin_commons_1.onAsk)("ask:device-manager:model:asset:get", async ({ engineGroups, engineId, model }) => {
22
- const assetModel = await this.getAsset(engineGroups, engineId, model);
26
+ (0, kuzzle_plugin_commons_1.onAsk)("ask:device-manager:model:asset:get", async ({ tenantClasses, index, model }) => {
27
+ const assetModel = await this.getAsset(tenantClasses, index, model);
23
28
  return assetModel._source;
24
29
  });
25
30
  (0, kuzzle_plugin_commons_1.onAsk)("ask:device-manager:model:device:get", async ({ model }) => {
@@ -30,8 +35,9 @@ class ModelService extends shared_1.BaseService {
30
35
  const groupModel = await this.getGroup(model);
31
36
  return groupModel._source;
32
37
  });
33
- (0, kuzzle_plugin_commons_1.onAsk)("ask:device-manager:model:measure:get", async ({ type, engineId }) => {
34
- const measureModel = await this.getMeasure(type, engineId);
38
+ (0, kuzzle_plugin_commons_1.onAsk)("ask:device-manager:model:metadata-referential:get", async () => this.getMetadataReferential());
39
+ (0, kuzzle_plugin_commons_1.onAsk)("ask:device-manager:model:measure:get", async ({ type, index }) => {
40
+ const measureModel = await this.getMeasure(type, index);
35
41
  return measureModel._source;
36
42
  });
37
43
  const genericModelsHandler = async (documents, request) => {
@@ -155,160 +161,180 @@ class ModelService extends shared_1.BaseService {
155
161
  }
156
162
  return { models, strict, type };
157
163
  }
158
- async writeAsset(engineGroups, model, metadataMappings, defaultMetadata, metadataDetails, metadataGroups, measures, tooltipModels, locales, engineIds, icon) {
159
- if (kuzzle_1.Inflector.pascalCase(model) !== model) {
160
- throw new kuzzle_1.BadRequestError(`Asset model "${model}" must be PascalCase.`);
161
- }
162
- if (engineIds?.length && engineGroups?.length) {
163
- throw new kuzzle_1.BadRequestError(`"engineIds" and "engineGroups" 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 normalizedEngineGroups = engineGroups.includes("commons")
170
- ? ["commons"]
171
- : engineGroups;
172
- // Anti-shadowing: reject if a model with the same name exists at a different scope level
173
- const isGlobal = normalizedEngineGroups.includes("commons") &&
174
- (!engineIds || engineIds.length === 0);
175
- const isTenantScoped = engineIds && engineIds.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.engineGroups?.includes("commons") &&
191
- (!doc.engineIds || doc.engineIds.length === 0);
192
- const docIsTenantScoped = doc.engineIds && doc.engineIds.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;
164
+ async writeAsset(tenantClasses, model, metadataMappings, defaultMetadata, metadataDetails, metadataGroups, measures, tooltipModels, locales, indexes, icon) {
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
- ...(engineIds?.length
231
- ? { engineIds }
232
- : { engineGroups: normalizedEngineGroups }),
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
- async writeGroup(engineGroups, 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 (engineGroups.includes("commons")) {
285
- engineGroups = ["commons"];
286
- }
287
- const groupAffinity = this.checkGroupAffinity(affinity);
288
- const modelContent = {
289
- engineGroups,
290
- group: {
291
- affinity: groupAffinity,
298
+ async writeGroup(tenantClasses, model, affinity, metadataMappings, defaultMetadata, metadataDetails, metadataGroups, icon, locales) {
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
- async writeMeasure(type, valuesMappings, validationSchema, valuesDetails, locales, engineIds, icon) {
337
+ async writeMeasure(type, valuesMappings, validationSchema, valuesDetails, locales, indexes, icon) {
312
338
  const modelContent = {
313
339
  measure: {
314
340
  icon,
@@ -319,11 +345,11 @@ class ModelService extends shared_1.BaseService {
319
345
  },
320
346
  type: "measure",
321
347
  };
322
- if (engineIds?.length) {
323
- modelContent.engineIds = engineIds;
348
+ if (indexes?.length) {
349
+ modelContent.indexes = indexes;
324
350
  }
325
351
  // Anti-shadowing: a measure type must be either global or tenant-scoped, not both
326
- await this.checkMeasureShadowing(type, engineIds);
352
+ await this.checkMeasureShadowing(type, indexes);
327
353
  if (validationSchema) {
328
354
  try {
329
355
  (0, AJValidator_1.addSchemaToCache)(type, validationSchema);
@@ -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) {
@@ -355,7 +383,7 @@ class ModelService extends shared_1.BaseService {
355
383
  await this.sdk.document.delete(this.config.platformIndex, plugin_1.InternalCollection.MODELS, _id);
356
384
  }
357
385
  /**
358
- * List all asset models regardless of engine group scope.
386
+ * List all asset models regardless of tenant class scope.
359
387
  * Used for super admin global view.
360
388
  */
361
389
  async listAllAssets() {
@@ -365,14 +393,14 @@ class ModelService extends shared_1.BaseService {
365
393
  /**
366
394
  * Check if an engine (tenant) exists by its ID.
367
395
  */
368
- async engineExists(engineId) {
396
+ async engineExists(index) {
369
397
  try {
370
398
  const result = await this.sdk.document.search(this.config.platformIndex, plugin_1.InternalCollection.CONFIG, {
371
399
  query: {
372
400
  bool: {
373
401
  must: [
374
402
  { term: { type: "engine-device-manager" } },
375
- { term: { "engine.index": engineId } },
403
+ { term: { "engine.index": index } },
376
404
  ],
377
405
  },
378
406
  },
@@ -384,27 +412,11 @@ class ModelService extends shared_1.BaseService {
384
412
  }
385
413
  }
386
414
  /**
387
- * Check if the requesting user has access to an engine.
388
- */
389
- async userHasEngineAccess(request, engineId) {
390
- try {
391
- const result = await this.sdk.security.checkRights(request.context.user?._id, {
392
- controller: "device-manager/assets",
393
- action: "get",
394
- index: engineId,
395
- });
396
- return result;
397
- }
398
- catch {
399
- return false;
400
- }
401
- }
402
- /**
403
- * Find engine groups that don't exist.
415
+ * Find tenant classes that don't exist.
404
416
  */
405
- async findInvalidEngineGroups(engineGroups) {
417
+ async findInvalidTenantClasses(tenantClasses) {
406
418
  // "commons" is always valid
407
- const toCheck = engineGroups.filter((g) => g !== "commons");
419
+ const toCheck = tenantClasses.filter((g) => g !== "commons");
408
420
  if (toCheck.length === 0) {
409
421
  return [];
410
422
  }
@@ -413,11 +425,11 @@ class ModelService extends shared_1.BaseService {
413
425
  equals: { type: "engine-device-manager" },
414
426
  },
415
427
  }, { lang: "koncorde", size: 5000 });
416
- const existingGroups = new Set(result.hits.map((h) => h._source.engine?.group));
417
- return toCheck.filter((g) => !existingGroups.has(g));
428
+ const existingTenantClasses = new Set(result.hits.map((h) => h._source.engine?.group));
429
+ return toCheck.filter((g) => !existingTenantClasses.has(g));
418
430
  }
419
- async listAsset(engineGroups, engineId) {
420
- const result = await this.searchAssets(engineGroups, engineId, {
431
+ async listAsset(tenantClasses, index) {
432
+ const result = await this.searchAssets(tenantClasses, index, {
421
433
  searchBody: {
422
434
  sort: { "asset.model": "asc" },
423
435
  },
@@ -434,8 +446,8 @@ class ModelService extends shared_1.BaseService {
434
446
  });
435
447
  return result.hits;
436
448
  }
437
- async listGroups(engineGroups) {
438
- const result = await this.searchGroups(engineGroups, {
449
+ async listGroups(tenantClasses) {
450
+ const result = await this.searchGroups(tenantClasses, {
439
451
  searchBody: {
440
452
  sort: { "group.model": "asc" },
441
453
  },
@@ -443,8 +455,8 @@ class ModelService extends shared_1.BaseService {
443
455
  });
444
456
  return result.hits;
445
457
  }
446
- async listMeasures(engineId) {
447
- const result = await this.searchMeasures(engineId, {
458
+ async listMeasures(index) {
459
+ const result = await this.searchMeasures(index, {
448
460
  searchBody: {
449
461
  sort: { "measure.type": "asc" },
450
462
  },
@@ -452,26 +464,26 @@ class ModelService extends shared_1.BaseService {
452
464
  });
453
465
  return result.hits;
454
466
  }
455
- async searchAssets(engineGroups, engineId, searchParams) {
456
- const scopeFilter = engineId
467
+ async searchAssets(tenantClasses, index, searchParams) {
468
+ const scopeFilter = index
457
469
  ? {
458
470
  bool: {
459
471
  should: [
460
472
  {
461
473
  bool: {
462
- must: [{ term: { engineIds: engineId } }],
474
+ must: [{ term: { indexes: index } }],
463
475
  },
464
476
  },
465
477
  {
466
478
  bool: {
467
- must: [{ terms: { engineGroups } }],
468
- must_not: [{ exists: { field: "engineIds" } }],
479
+ must: [{ terms: { tenantClasses } }],
480
+ must_not: [{ exists: { field: "indexes" } }],
469
481
  },
470
482
  },
471
483
  {
472
484
  bool: {
473
- must: [{ term: { engineGroups: "commons" } }],
474
- must_not: [{ exists: { field: "engineIds" } }],
485
+ must: [{ term: { tenantClasses: "commons" } }],
486
+ must_not: [{ exists: { field: "indexes" } }],
475
487
  },
476
488
  },
477
489
  ],
@@ -482,14 +494,14 @@ class ModelService extends shared_1.BaseService {
482
494
  should: [
483
495
  {
484
496
  bool: {
485
- must: [{ terms: { engineGroups } }],
486
- must_not: [{ exists: { field: "engineIds" } }],
497
+ must: [{ terms: { tenantClasses } }],
498
+ must_not: [{ exists: { field: "indexes" } }],
487
499
  },
488
500
  },
489
501
  {
490
502
  bool: {
491
- must: [{ term: { engineGroups: "commons" } }],
492
- must_not: [{ exists: { field: "engineIds" } }],
503
+ must: [{ term: { tenantClasses: "commons" } }],
504
+ must_not: [{ exists: { field: "indexes" } }],
493
505
  },
494
506
  },
495
507
  ],
@@ -533,7 +545,7 @@ class ModelService extends shared_1.BaseService {
533
545
  size: searchParams.size,
534
546
  });
535
547
  }
536
- async searchGroups(engineGroups, searchParams) {
548
+ async searchGroups(tenantClasses, searchParams) {
537
549
  const query = {
538
550
  bool: {
539
551
  must: [
@@ -542,8 +554,8 @@ class ModelService extends shared_1.BaseService {
542
554
  {
543
555
  bool: {
544
556
  should: [
545
- { terms: { engineGroups } },
546
- { term: { engineGroups: "commons" } },
557
+ { terms: { tenantClasses } },
558
+ { term: { tenantClasses: "commons" } },
547
559
  ],
548
560
  },
549
561
  },
@@ -560,19 +572,19 @@ class ModelService extends shared_1.BaseService {
560
572
  size: searchParams.size,
561
573
  });
562
574
  }
563
- async searchMeasures(engineId, searchParams) {
564
- const scopeFilter = engineId
575
+ async searchMeasures(index, searchParams) {
576
+ const scopeFilter = index
565
577
  ? {
566
578
  bool: {
567
579
  should: [
568
- { term: { engineIds: engineId } },
569
- { bool: { must_not: [{ exists: { field: "engineIds" } }] } },
580
+ { term: { indexes: index } },
581
+ { bool: { must_not: [{ exists: { field: "indexes" } }] } },
570
582
  ],
571
583
  },
572
584
  }
573
585
  : {
574
586
  bool: {
575
- must_not: [{ exists: { field: "engineIds" } }],
587
+ must_not: [{ exists: { field: "indexes" } }],
576
588
  },
577
589
  };
578
590
  const query = {
@@ -614,16 +626,16 @@ class ModelService extends shared_1.BaseService {
614
626
  const result = await this.sdk.document.search(this.config.platformIndex, plugin_1.InternalCollection.MODELS, { query }, { lang: "koncorde", size: 1 });
615
627
  return result.total > 0;
616
628
  }
617
- async getAsset(engineGroups, engineId, model) {
629
+ async getAsset(tenantClasses, index, model) {
618
630
  const baseFilter = [
619
631
  { term: { type: "asset" } },
620
632
  { term: { "asset.model": model } },
621
633
  ];
622
634
  // Priority 1: tenant-scoped model for this specific engine
623
- if (engineId) {
635
+ if (index) {
624
636
  const tenantQuery = {
625
637
  bool: {
626
- must: [...baseFilter, { term: { engineIds: engineId } }],
638
+ must: [...baseFilter, { term: { indexes: index } }],
627
639
  },
628
640
  };
629
641
  const tenantResult = await this.sdk.document.search(this.config.platformIndex, plugin_1.InternalCollection.MODELS, { query: tenantQuery }, { lang: "elasticsearch", size: 1 });
@@ -631,11 +643,11 @@ class ModelService extends shared_1.BaseService {
631
643
  return tenantResult.hits[0];
632
644
  }
633
645
  }
634
- // Priority 2: group-scoped model (no engineIds field)
646
+ // Priority 2: group-scoped model (no indexes field)
635
647
  const groupQuery = {
636
648
  bool: {
637
- must: [...baseFilter, { terms: { engineGroups } }],
638
- must_not: [{ exists: { field: "engineIds" } }],
649
+ must: [...baseFilter, { terms: { tenantClasses } }],
650
+ must_not: [{ exists: { field: "indexes" } }],
639
651
  },
640
652
  };
641
653
  const groupResult = await this.sdk.document.search(this.config.platformIndex, plugin_1.InternalCollection.MODELS, { query: groupQuery }, { lang: "elasticsearch", size: 1 });
@@ -645,15 +657,15 @@ class ModelService extends shared_1.BaseService {
645
657
  // Priority 3: commons model
646
658
  const commonsQuery = {
647
659
  bool: {
648
- must: [...baseFilter, { term: { engineGroups: "commons" } }],
649
- must_not: [{ exists: { field: "engineIds" } }],
660
+ must: [...baseFilter, { term: { tenantClasses: "commons" } }],
661
+ must_not: [{ exists: { field: "indexes" } }],
650
662
  },
651
663
  };
652
664
  const commonsResult = await this.sdk.document.search(this.config.platformIndex, plugin_1.InternalCollection.MODELS, { query: commonsQuery }, { lang: "elasticsearch", size: 1 });
653
665
  if (commonsResult.total > 0) {
654
666
  return commonsResult.hits[0];
655
667
  }
656
- throw new kuzzle_1.NotFoundError(`Unknown Asset model "${model}" for engineGroups ${engineGroups.join(", ")}.`);
668
+ throw new kuzzle_1.NotFoundError(`Unknown Asset model "${model}" for tenantClasses ${tenantClasses.join(", ")}.`);
657
669
  }
658
670
  async getDevice(model) {
659
671
  const query = {
@@ -684,23 +696,23 @@ class ModelService extends shared_1.BaseService {
684
696
  }
685
697
  return result.hits[0];
686
698
  }
687
- async getMeasure(type, engineId) {
699
+ async getMeasure(type, index) {
688
700
  const baseFilter = [
689
701
  { term: { type: "measure" } },
690
702
  { term: { "measure.type": type } },
691
703
  ];
692
- const scopeFilter = engineId
704
+ const scopeFilter = index
693
705
  ? {
694
706
  bool: {
695
707
  should: [
696
- { term: { engineIds: engineId } },
697
- { bool: { must_not: [{ exists: { field: "engineIds" } }] } },
708
+ { term: { indexes: index } },
709
+ { bool: { must_not: [{ exists: { field: "indexes" } }] } },
698
710
  ],
699
711
  },
700
712
  }
701
713
  : {
702
714
  bool: {
703
- must_not: [{ exists: { field: "engineIds" } }],
715
+ must_not: [{ exists: { field: "indexes" } }],
704
716
  },
705
717
  };
706
718
  const result = await this.sdk.document.search(this.config.platformIndex, plugin_1.InternalCollection.MODELS, {
@@ -719,8 +731,8 @@ class ModelService extends shared_1.BaseService {
719
731
  * Checks that creating/updating a measure model does not shadow an existing
720
732
  * one at a different scope (global vs tenant-scoped).
721
733
  */
722
- async checkMeasureShadowing(type, engineIds) {
723
- const isNewTenantScoped = engineIds && engineIds.length > 0;
734
+ async checkMeasureShadowing(type, indexes) {
735
+ const isNewTenantScoped = indexes && indexes.length > 0;
724
736
  // Check for existing measures of the same type at the opposite scope
725
737
  const conflictQuery = isNewTenantScoped
726
738
  ? {
@@ -730,7 +742,7 @@ class ModelService extends shared_1.BaseService {
730
742
  { term: { type: "measure" } },
731
743
  { term: { "measure.type": type } },
732
744
  ],
733
- must_not: [{ exists: { field: "engineIds" } }],
745
+ must_not: [{ exists: { field: "indexes" } }],
734
746
  },
735
747
  }
736
748
  : {
@@ -739,7 +751,7 @@ class ModelService extends shared_1.BaseService {
739
751
  must: [
740
752
  { term: { type: "measure" } },
741
753
  { term: { "measure.type": type } },
742
- { exists: { field: "engineIds" } },
754
+ { exists: { field: "indexes" } },
743
755
  ],
744
756
  },
745
757
  };
@@ -752,61 +764,334 @@ class ModelService extends shared_1.BaseService {
752
764
  /**
753
765
  * Update an asset model
754
766
  */
755
- async updateAsset(engineGroups, engineId, model, metadataMappings, defaultMetadata, metadataDetails, metadataGroups, measures, tooltipModels, locales, icon, request) {
756
- if (kuzzle_1.Inflector.pascalCase(model) !== model) {
757
- throw new kuzzle_1.BadRequestError(`Asset model "${model}" must be PascalCase.`);
758
- }
759
- this.checkDefaultValues(metadataMappings, defaultMetadata);
760
- const existingAsset = await this.getAsset(engineGroups, engineId, model);
761
- // The field must be deleted if an element of the table is to be deleted
762
- await this.sdk.document.deleteFields(this.config.platformIndex, plugin_1.InternalCollection.MODELS, existingAsset._id, [
763
- "asset.tooltipModels",
764
- "asset.metadataMappings",
765
- "asset.defaultMetadata",
766
- "asset.metadataDetails",
767
- "asset.metadataGroups",
768
- ], { source: true });
769
- const measuresUpdated = measures.length === 0 ? existingAsset._source.asset.measures : measures;
770
- // Preserve the existing scope: scope (engineGroups / engineIds) is decided
771
- // at write time and must not be silently rewritten by an update payload.
772
- // This also enforces engineGroups / engineIds mutual exclusivity (KZLPRD-1192).
773
- const assetModelContent = {
774
- asset: {
775
- defaultMetadata,
776
- icon: icon ?? existingAsset._source.asset.icon,
777
- locales,
778
- measures: measuresUpdated,
779
- metadataDetails,
780
- metadataGroups,
767
+ async updateAsset(tenantClasses, index, model, metadataMappings, defaultMetadata, metadataDetails, metadataGroups, measures, tooltipModels, locales, icon, request) {
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,
781
854
  metadataMappings,
782
- model,
783
- 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
+ ],
784
884
  },
785
- engineGroups: existingAsset._source.engineGroups,
786
- engineIds: existingAsset._source.engineIds,
787
- type: "asset",
788
- };
789
- const assetModel = {
790
- _id: existingAsset._id,
791
- _source: assetModelContent,
792
- };
793
- const conflicts = await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:engine:doesUpdateConflict", { twin: { models: [assetModelContent], type: "asset" } });
794
- if (conflicts.length > 0) {
795
- throw new MappingsConflictsError_1.MappingsConflictsError(`Assets mappings are causing conflicts`, conflicts);
796
- }
797
- const endDocument = await this.updateDocument(request, assetModel, {
798
- collection: plugin_1.InternalCollection.MODELS,
799
- engineId: this.config.platformIndex,
800
- }, { source: true });
801
- // ? Only update engineIds and refresh asset models when necessary
802
- 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);
803
931
  await this.sdk.collection.refresh(this.config.platformIndex, plugin_1.InternalCollection.MODELS);
804
- await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:engine:updateAll");
805
- await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:asset:refresh-model", {
806
- 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,
807
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`);
808
1094
  }
809
- return endDocument;
810
1095
  }
811
1096
  }
812
1097
  exports.ModelService = ModelService;