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.
- package/dist/bin/migration/migrate-tenant-classes.d.ts +2 -0
- package/dist/bin/migration/migrate-tenant-classes.js +133 -0
- package/dist/bin/migration/migrate-tenant-classes.js.map +1 -0
- package/dist/lib/modules/asset/AssetHistoryService.d.ts +1 -1
- package/dist/lib/modules/asset/AssetHistoryService.js +3 -3
- package/dist/lib/modules/asset/AssetHistoryService.js.map +1 -1
- package/dist/lib/modules/asset/AssetService.d.ts +18 -12
- package/dist/lib/modules/asset/AssetService.js +115 -115
- package/dist/lib/modules/asset/AssetService.js.map +1 -1
- package/dist/lib/modules/asset/AssetsController.d.ts +1 -1
- package/dist/lib/modules/asset/AssetsController.js +72 -74
- package/dist/lib/modules/asset/AssetsController.js.map +1 -1
- package/dist/lib/modules/asset/roles/RoleAssetsAdmin.d.ts +1 -0
- package/dist/lib/modules/asset/roles/RoleAssetsAdmin.js +2 -0
- package/dist/lib/modules/asset/roles/RoleAssetsAdmin.js.map +1 -1
- package/dist/lib/modules/asset/roles/RoleAssetsReader.d.ts +1 -0
- package/dist/lib/modules/asset/roles/RoleAssetsReader.js +2 -0
- package/dist/lib/modules/asset/roles/RoleAssetsReader.js.map +1 -1
- package/dist/lib/modules/asset/types/AssetApi.d.ts +6 -6
- package/dist/lib/modules/asset/types/AssetEvents.d.ts +1 -1
- package/dist/lib/modules/decoder/PayloadService.js +16 -16
- package/dist/lib/modules/decoder/PayloadService.js.map +1 -1
- package/dist/lib/modules/device/DeviceService.d.ts +11 -11
- package/dist/lib/modules/device/DeviceService.js +86 -86
- package/dist/lib/modules/device/DeviceService.js.map +1 -1
- package/dist/lib/modules/device/DevicesController.js +60 -64
- package/dist/lib/modules/device/DevicesController.js.map +1 -1
- package/dist/lib/modules/device/collections/deviceMappings.js +2 -2
- package/dist/lib/modules/device/collections/deviceMappings.js.map +1 -1
- package/dist/lib/modules/device/roles/RoleDevicesAdmin.d.ts +1 -0
- package/dist/lib/modules/device/roles/RoleDevicesAdmin.js +2 -0
- package/dist/lib/modules/device/roles/RoleDevicesAdmin.js.map +1 -1
- package/dist/lib/modules/device/roles/RoleDevicesReader.d.ts +1 -0
- package/dist/lib/modules/device/roles/RoleDevicesReader.js +2 -0
- package/dist/lib/modules/device/roles/RoleDevicesReader.js.map +1 -1
- package/dist/lib/modules/device/types/DeviceApi.d.ts +2 -2
- package/dist/lib/modules/device/types/DeviceContent.d.ts +2 -2
- package/dist/lib/modules/device/types/DeviceEvents.d.ts +1 -1
- package/dist/lib/modules/group/GroupsController.d.ts +3 -3
- package/dist/lib/modules/group/GroupsController.js +66 -66
- package/dist/lib/modules/group/GroupsController.js.map +1 -1
- package/dist/lib/modules/group/GroupsService.d.ts +19 -14
- package/dist/lib/modules/group/GroupsService.js +103 -64
- package/dist/lib/modules/group/GroupsService.js.map +1 -1
- package/dist/lib/modules/group/index.d.ts +1 -0
- package/dist/lib/modules/group/index.js +1 -0
- package/dist/lib/modules/group/index.js.map +1 -1
- package/dist/lib/modules/group/roles/RoleGroupsAdmin.d.ts +1 -0
- package/dist/lib/modules/group/roles/RoleGroupsAdmin.js +2 -0
- package/dist/lib/modules/group/roles/RoleGroupsAdmin.js.map +1 -1
- package/dist/lib/modules/group/roles/RoleGroupsReader.d.ts +1 -0
- package/dist/lib/modules/group/roles/RoleGroupsReader.js +2 -0
- package/dist/lib/modules/group/roles/RoleGroupsReader.js.map +1 -1
- package/dist/lib/modules/group/types/GroupEvents.d.ts +8 -0
- package/dist/lib/modules/group/types/GroupEvents.js +3 -0
- package/dist/lib/modules/group/types/GroupEvents.js.map +1 -0
- package/dist/lib/modules/group/types/GroupsApi.d.ts +1 -1
- package/dist/lib/modules/measure/MeasureExporter.d.ts +5 -5
- package/dist/lib/modules/measure/MeasureExporter.js +20 -20
- package/dist/lib/modules/measure/MeasureExporter.js.map +1 -1
- package/dist/lib/modules/measure/MeasureService.d.ts +2 -2
- package/dist/lib/modules/measure/MeasureService.js +5 -5
- package/dist/lib/modules/measure/MeasureService.js.map +1 -1
- package/dist/lib/modules/measure/MeasureTargetBuilder.d.ts +1 -1
- package/dist/lib/modules/measure/MeasureTargetBuilder.js +2 -2
- package/dist/lib/modules/measure/MeasureTargetBuilder.js.map +1 -1
- package/dist/lib/modules/measure/types/MeasureTarget.d.ts +1 -1
- package/dist/lib/modules/measure/types/MeasureTarget.js +1 -1
- package/dist/lib/modules/measure/types/MeasureTarget.js.map +1 -1
- package/dist/lib/modules/model/MetadataReferential.d.ts +119 -0
- package/dist/lib/modules/model/MetadataReferential.js +342 -0
- package/dist/lib/modules/model/MetadataReferential.js.map +1 -0
- package/dist/lib/modules/model/ModelSerializer.js +13 -13
- package/dist/lib/modules/model/ModelSerializer.js.map +1 -1
- package/dist/lib/modules/model/ModelService.d.ts +120 -22
- package/dist/lib/modules/model/ModelService.js +548 -263
- package/dist/lib/modules/model/ModelService.js.map +1 -1
- package/dist/lib/modules/model/ModelsConflicts.d.ts +23 -0
- package/dist/lib/modules/model/ModelsConflicts.js +54 -4
- package/dist/lib/modules/model/ModelsConflicts.js.map +1 -1
- package/dist/lib/modules/model/ModelsController.d.ts +11 -2
- package/dist/lib/modules/model/ModelsController.js +151 -54
- package/dist/lib/modules/model/ModelsController.js.map +1 -1
- package/dist/lib/modules/model/ModelsRegister.d.ts +37 -5
- package/dist/lib/modules/model/ModelsRegister.js +107 -9
- package/dist/lib/modules/model/ModelsRegister.js.map +1 -1
- package/dist/lib/modules/model/collections/modelsMappings.js +15 -1
- package/dist/lib/modules/model/collections/modelsMappings.js.map +1 -1
- package/dist/lib/modules/model/types/ModelApi.d.ts +105 -14
- package/dist/lib/modules/model/types/ModelContent.d.ts +73 -7
- package/dist/lib/modules/model/types/ModelContent.js.map +1 -1
- package/dist/lib/modules/model/types/ModelDefinition.d.ts +6 -3
- package/dist/lib/modules/model/types/ModelEvents.d.ts +9 -4
- package/dist/lib/modules/plugin/DeviceManagerEngine.d.ts +58 -25
- package/dist/lib/modules/plugin/DeviceManagerEngine.js +184 -88
- package/dist/lib/modules/plugin/DeviceManagerEngine.js.map +1 -1
- package/dist/lib/modules/plugin/DeviceManagerPlugin.d.ts +46 -4
- package/dist/lib/modules/plugin/DeviceManagerPlugin.js +72 -9
- package/dist/lib/modules/plugin/DeviceManagerPlugin.js.map +1 -1
- package/dist/lib/modules/plugin/types/DeviceManagerConfiguration.d.ts +11 -0
- package/dist/lib/modules/shared/services/AbstractExporter.d.ts +8 -8
- package/dist/lib/modules/shared/services/AbstractExporter.js +9 -9
- package/dist/lib/modules/shared/services/AbstractExporter.js.map +1 -1
- package/dist/lib/modules/shared/services/BaseService.d.ts +7 -7
- package/dist/lib/modules/shared/services/BaseService.js +20 -17
- package/dist/lib/modules/shared/services/BaseService.js.map +1 -1
- package/dist/lib/modules/shared/services/DigitalTwinExporter.d.ts +3 -3
- package/dist/lib/modules/shared/services/DigitalTwinExporter.js +18 -18
- package/dist/lib/modules/shared/services/DigitalTwinExporter.js.map +1 -1
- package/dist/lib/modules/shared/services/DigitalTwinService.d.ts +8 -8
- package/dist/lib/modules/shared/services/DigitalTwinService.js +39 -39
- package/dist/lib/modules/shared/services/DigitalTwinService.js.map +1 -1
- package/dist/lib/modules/shared/types/DigitalTwinApi.d.ts +1 -1
- package/dist/lib/modules/shared/types/DigitalTwinEvents.d.ts +2 -2
- package/dist/lib/modules/shared/utils/lock.d.ts +7 -1
- package/dist/lib/modules/shared/utils/lock.js +2 -2
- package/dist/lib/modules/shared/utils/lock.js.map +1 -1
- 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 ({
|
|
22
|
-
const assetModel = await this.getAsset(
|
|
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:
|
|
34
|
-
|
|
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(
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
-
|
|
212
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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,
|
|
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 (
|
|
323
|
-
modelContent.
|
|
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,
|
|
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
|
|
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 (
|
|
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
|
|
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(
|
|
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":
|
|
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
|
-
*
|
|
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
|
|
417
|
+
async findInvalidTenantClasses(tenantClasses) {
|
|
406
418
|
// "commons" is always valid
|
|
407
|
-
const toCheck =
|
|
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
|
|
417
|
-
return toCheck.filter((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(
|
|
420
|
-
const result = await this.searchAssets(
|
|
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(
|
|
438
|
-
const result = await this.searchGroups(
|
|
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(
|
|
447
|
-
const result = await this.searchMeasures(
|
|
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(
|
|
456
|
-
const scopeFilter =
|
|
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: {
|
|
474
|
+
must: [{ term: { indexes: index } }],
|
|
463
475
|
},
|
|
464
476
|
},
|
|
465
477
|
{
|
|
466
478
|
bool: {
|
|
467
|
-
must: [{ terms: {
|
|
468
|
-
must_not: [{ exists: { field: "
|
|
479
|
+
must: [{ terms: { tenantClasses } }],
|
|
480
|
+
must_not: [{ exists: { field: "indexes" } }],
|
|
469
481
|
},
|
|
470
482
|
},
|
|
471
483
|
{
|
|
472
484
|
bool: {
|
|
473
|
-
must: [{ term: {
|
|
474
|
-
must_not: [{ exists: { field: "
|
|
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: {
|
|
486
|
-
must_not: [{ exists: { field: "
|
|
497
|
+
must: [{ terms: { tenantClasses } }],
|
|
498
|
+
must_not: [{ exists: { field: "indexes" } }],
|
|
487
499
|
},
|
|
488
500
|
},
|
|
489
501
|
{
|
|
490
502
|
bool: {
|
|
491
|
-
must: [{ term: {
|
|
492
|
-
must_not: [{ exists: { field: "
|
|
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(
|
|
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: {
|
|
546
|
-
{ term: {
|
|
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(
|
|
564
|
-
const scopeFilter =
|
|
575
|
+
async searchMeasures(index, searchParams) {
|
|
576
|
+
const scopeFilter = index
|
|
565
577
|
? {
|
|
566
578
|
bool: {
|
|
567
579
|
should: [
|
|
568
|
-
{ term: {
|
|
569
|
-
{ bool: { must_not: [{ exists: { field: "
|
|
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: "
|
|
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(
|
|
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 (
|
|
635
|
+
if (index) {
|
|
624
636
|
const tenantQuery = {
|
|
625
637
|
bool: {
|
|
626
|
-
must: [...baseFilter, { term: {
|
|
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
|
|
646
|
+
// Priority 2: group-scoped model (no indexes field)
|
|
635
647
|
const groupQuery = {
|
|
636
648
|
bool: {
|
|
637
|
-
must: [...baseFilter, { terms: {
|
|
638
|
-
must_not: [{ exists: { field: "
|
|
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: {
|
|
649
|
-
must_not: [{ exists: { field: "
|
|
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
|
|
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,
|
|
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 =
|
|
704
|
+
const scopeFilter = index
|
|
693
705
|
? {
|
|
694
706
|
bool: {
|
|
695
707
|
should: [
|
|
696
|
-
{ term: {
|
|
697
|
-
{ bool: { must_not: [{ exists: { field: "
|
|
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: "
|
|
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,
|
|
723
|
-
const isNewTenantScoped =
|
|
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: "
|
|
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: "
|
|
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(
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
783
|
-
|
|
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
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
};
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
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 (
|
|
805
|
-
await (0, kuzzle_plugin_commons_1.ask)("ask:device-manager:
|
|
806
|
-
|
|
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;
|