diff --git a/doc/1/controllers/sensor/m-attach/index.md b/doc/1/controllers/sensor/m-attach/index.md new file mode 100644 index 00000000..5c759c25 --- /dev/null +++ b/doc/1/controllers/sensor/m-attach/index.md @@ -0,0 +1,88 @@ +--- +code: true +type: page +title: mAttach +description: Attach multiple sensors to multiple tenants index +--- + +# mAttach + +Attach multiple sensors to multiple tenants. + +The sensor document will be duplicated inside the tenant "sensors" collection. + +--- + +## Query Syntax + +### HTTP + +``` http +URL: http://kuzzle:7512/_/device-manager/sensors/_mAttach +Method: PUT +Body: +``` + +``` js +{ + // Using JSON + "records" [{ + "tenantId": "tenant-kuzzle", + "sensorId": "test-id" + }], + // Using CSV syntax + "csv": "tenant,id\ntenant-kuzzle,test-id" +} +``` + +### Other protocols + +``` js +{ + "controller": "device-manager/sensor", + "action": "mAttach", + "body": { + // Using JSON + "records" [{ + "tenantId": "tenant-kuzzle", + "sensorId": "test-id" + }], + // Using CSV syntax + "csv": "tenantId,sensorId\ntenant-kuzzle,test-id", + } +} +``` + +--- + +## Body properties + +Body properties, must contain at least one of + +- `records`: an array of object containing `tenantId` and `sensorId` +- `csv`: a csv syntax compatible containing at least this two headers `tenantId,sensorId` with their corresponding values +- `strict`: a boolean value that indicate if the process should fail at first error + +--- + +### Optional: + +* `refresh`: if set to `wait_for`, Kuzzle will not respond until the documents are indexed + +--- + +## Response + +``` js +{ + "status": 200, + "error": null, + "controller": "device-manager/sensor", + "action": "mAttach", + "requestId": "", + "result": { + "errors": [], + "successes": [] + } +} +``` diff --git a/doc/1/guides/sensors/index.md b/doc/1/guides/sensors/index.md index 92708772..867b3f62 100644 --- a/doc/1/guides/sensors/index.md +++ b/doc/1/guides/sensors/index.md @@ -110,6 +110,14 @@ Sensors can be attached to tenant by using the [device-manager/sensor:attach](/k When attached, the sensor document is copied inside the `sensors` collection of the tenant index. +## Attach to multiple tenant + +Multiple different Sensors can also be attached to multiple defferents tenant by using the [device-manager/sensor:mAttach](/kuzzle-iot-platform/device-manager/1/controllers/sensor/m-attach) API action. + +The format used can be either __CSV__ in the form of a string in the format `tenantId,sensorId\nmytenantId,mysensorId` or __JSON__ in the form of an array of objects `"records": [{ "tenantId": "mytenantId", "sensorId": "mysensorId"}]`. + +When attached, all sensors documents are copied inside the `sensors` collections of all different tenant index. + ## Link to an asset Sensors can be linked to an asset by using the [device-manager/sensor:link](/kuzzle-iot-platform/device-manager/1/controllers/sensor/link) API action. diff --git a/features/SensorController.feature b/features/SensorController.feature index 7ce5b66b..9f7471d9 100644 --- a/features/SensorController.feature +++ b/features/SensorController.feature @@ -9,6 +9,37 @@ Feature: Device Manager sensor controller | tenantId | "tenant-kuzzle" | And The document "tenant-kuzzle":"sensors":"DummyTemp_detached" exists + Scenario: Attach multiple sensors to a tenant using JSON + Given an engine on index "tenant-kuzzle" + When I successfully execute the action "device-manager/sensor":"mAttach" with args: + | body.records.0.tenantId | "tenant-kuzzle" | + | body.records.0.sensorId | "DummyTemp_detached" | + | body.records.1.tenantId | "tenant-kuzzle" | + | body.records.1.sensorId | "DummyTemp_attached-ayse-unlinked" | + Then The document "device-manager":"sensors":"DummyTemp_detached" content match: + | tenantId | "tenant-kuzzle" | + Then The document "device-manager":"sensors":"DummyTemp_attached-ayse-unlinked" content match: + | tenantId | "tenant-kuzzle" | + And The document "tenant-kuzzle":"sensors":"DummyTemp_detached" exists + And The document "tenant-kuzzle":"sensors":"DummyTemp_attached-ayse-unlinked" exists + + Scenario: Attach multiple sensor to a tenant using CSV + Given an engine on index "tenant-kuzzle" + When I successfully execute the action "device-manager/sensor":"mAttach" with args: + | body.csv | "tenantId,sensorId\\ntenant-kuzzle,DummyTemp_detached\\ntenant-kuzzle,DummyTemp_attached-ayse-unlinked," | + Then The document "device-manager":"sensors":"DummyTemp_detached" content match: + | tenantId | "tenant-kuzzle" | + Then The document "device-manager":"sensors":"DummyTemp_attached-ayse-unlinked" content match: + | tenantId | "tenant-kuzzle" | + And The document "tenant-kuzzle":"sensors":"DummyTemp_detached" exists + And The document "tenant-kuzzle":"sensors":"DummyTemp_attached-ayse-unlinked" exists + + Scenario: Attach multiple sensor to a tenant while exceeding documentsWriteCount limit + Given an engine on index "tenant-kuzzle" + When I attach multiple sensors while exeding documentsWriteCount limit + Then All attached sensors have the correct tenantId + Then All tenant sensors documents exists + Scenario: Error when assigning a sensor to a tenant Given an engine on index "tenant-kuzzle" When I execute the action "device-manager/sensor":"attachTenant" with args: @@ -23,7 +54,7 @@ Feature: Device Manager sensor controller | _id | "DummyTemp_detached" | | index | "tenant-kuzzle" | Then I should receive an error matching: - | message | "Sensor \"DummyTemp_detached\" is already attached to a tenant" | + | message | "These sensors \"DummyTemp_detached\" are already attached to a tenant" | Scenario: Detach sensor from a tenant Given an engine on index "tenant-kuzzle" diff --git a/features/fixtures/application/app.ts b/features/fixtures/application/app.ts index 8e575845..53d9dd0b 100644 --- a/features/fixtures/application/app.ts +++ b/features/fixtures/application/app.ts @@ -39,6 +39,7 @@ app.hook.register('request:onError', async (request: KuzzleRequest) => { }); app.config.set('plugins.kuzzle-plugin-logger.services.stdout.level', 'debug'); +app.config.set('limits.documentsWriteCount', 20); app.start() .then(() => { diff --git a/features/fixtures/fixtures.js b/features/fixtures/fixtures.js index bbdd9528..6f330db7 100644 --- a/features/fixtures/fixtures.js +++ b/features/fixtures/fixtures.js @@ -1,6 +1,9 @@ +const sensors = require('./sensors'); + module.exports = { 'device-manager': { sensors: [ + ...sensors, { index: { _id: 'DummyTemp_detached' } }, { reference: 'detached', diff --git a/features/fixtures/sensors.js b/features/fixtures/sensors.js new file mode 100644 index 00000000..7afa47ed --- /dev/null +++ b/features/fixtures/sensors.js @@ -0,0 +1,14 @@ +const sensors = []; +for (let i = 0; i < 50; i++) { + sensors.push({ index: { _id: `DummyTemp_detached-${i}` } }); + sensors.push({ + reference: 'detached', + model: `DummyTemp-${i}`, + measures: {}, + metadata: {}, + tenantId: null, + assetId: null + }); +} + +module.exports = sensors; diff --git a/features/step_definitions/sensor-controller-steps.js b/features/step_definitions/sensor-controller-steps.js new file mode 100644 index 00000000..cea01c1b --- /dev/null +++ b/features/step_definitions/sensor-controller-steps.js @@ -0,0 +1,52 @@ +const { When, Then } = require('cucumber'); + +When('I attach multiple sensors while exeding documentsWriteCount limit', async function () { + const records = []; + for (let i = 0; i < 50; i++) { + records.push({ sensorId: `DummyTemp_detached-${i}`, tenantId: 'tenant-kuzzle' }); + } + + + await this.sdk.query({ + controller: "device-manager/sensor", + action: "mAttach", + body: { + records + } + }); +}); + +Then('All attached sensors have the correct tenantId', async function () { + const sensorIds = []; + for (let i = 0; i < 50; i++) { + sensorIds.push(`DummyTemp_detached-${i}`); + } + + const { successes, errors } = await this.sdk.document.mGet('device-manager', 'sensors', sensorIds); + + if (errors.length > 0) { + throw new Error(errors); + } + + for (let i = 0; i < successes.length; i++) { + const { _source } = successes[i]; + if (_source.tenantId !== 'tenant-kuzzle') { + throw new Error('tenantId should be tenant-kuzzle but current value is: ', _source.tenantId); + } + } +}); + + +Then('All tenant sensors documents exists', async function () { + const sensorIds = []; + for (let i = 0; i < 50; i++) { + sensorIds.push(`DummyTemp_detached-${i}`); + } + + const { errors } = await this.sdk.document.mGet('tenant-kuzzle', 'sensors', sensorIds); + + if (errors.length > 0) { + throw new Error(errors); + } +}); + diff --git a/lib/controllers/SensorController.ts b/lib/controllers/SensorController.ts index 30b14c0f..849d07a7 100644 --- a/lib/controllers/SensorController.ts +++ b/lib/controllers/SensorController.ts @@ -1,23 +1,27 @@ +import csv from 'csvtojson'; import { KuzzleRequest, EmbeddedSDK, JSONObject, PluginContext, + BadRequestError } from 'kuzzle'; + import { CRUDController } from './CRUDController'; import { Decoder } from '../decoders'; import { Sensor } from '../models'; +import { SensorBulkContent } from '../types'; import { SensorService } from '../services'; export class SensorController extends CRUDController { private decoders: Map; - get sdk (): EmbeddedSDK { + get sdk(): EmbeddedSDK { return this.context.accessors.sdk; } - constructor (config: JSONObject, context: PluginContext, decoders: Map, sensorService: SensorService) { + constructor(config: JSONObject, context: PluginContext, decoders: Map, sensorService: SensorService) { super(config, context, 'sensors'); this.decoders = decoders; @@ -41,6 +45,10 @@ export class SensorController extends CRUDController { handler: this.attachTenant.bind(this), http: [{ verb: 'put', path: 'device-manager/:index/sensors/:_id/_attach' }] }, + mAttach: { + handler: this.mAttach.bind(this), + http: [{ verb: 'put', path: 'device-manager/sensors/_mAttach' }] + }, detach: { handler: this.detach.bind(this), http: [{ verb: 'delete', path: 'device-manager/sensors/:_id/_detach' }] @@ -64,11 +72,25 @@ export class SensorController extends CRUDController { const tenantId = this.getIndex(request); const sensorId = this.getId(request); - const sensor = await this.getSensor(sensorId); + const document = { tenantId: tenantId, sensorId: sensorId }; + const sensors = await this.mGetSensor([document]); - await this.sensorService.attachTenant(sensor, tenantId); + await this.sensorService.mAttach(sensors, [document], { strict: true }); } + /** + * Attach multiple sensors to multiple tenants + */ + async mAttach (request: KuzzleRequest) { + const { bulkData, strict } = await this.mParseRequest(request); + + const sensors = await this.mGetSensor(bulkData); + + return this.sensorService.mAttach(sensors, bulkData, { strict }); + } + + + /** * Unattach a sensor from it's tenant */ @@ -83,7 +105,7 @@ export class SensorController extends CRUDController { /** * Link a sensor to an asset. */ - async linkAsset (request: KuzzleRequest) { + async linkAsset(request: KuzzleRequest) { const assetId = this.getString(request, 'assetId'); const sensorId = this.getId(request); @@ -111,4 +133,37 @@ export class SensorController extends CRUDController { return new Sensor(document._source, document._id); } + + private async mGetSensor (sensors: SensorBulkContent[]): Promise { + const sensorIds = sensors.map(doc => doc.sensorId); + const result: any = await this.sdk.document.mGet( + this.config.adminIndex, + 'sensors', + sensorIds + ) + return result.successes.map((document: any) => new Sensor(document._source, document._id)); + } + + private async mParseRequest (request: KuzzleRequest) { + const { body } = request.input; + + let bulkData: SensorBulkContent[]; + + if (body.csv) { + const lines = await csv({ delimiter: 'auto' }) + .fromString(body.csv); + + bulkData = lines.map(line => ({ tenantId: line.tenantId, sensorId: line.sensorId })); + } + else if (body.records) { + bulkData = body.records; + } + else { + throw new BadRequestError(`Malformed request missing property csv or records`); + } + + const strict = body.strict || false; + + return { strict, bulkData }; + } } diff --git a/lib/services/SensorService.ts b/lib/services/SensorService.ts index 9fd852ba..ae82ff0d 100644 --- a/lib/services/SensorService.ts +++ b/lib/services/SensorService.ts @@ -1,137 +1,247 @@ import { - JSONObject, - PluginContext, - EmbeddedSDK, - BadRequestError, + JSONObject, + PluginContext, + EmbeddedSDK, + BadRequestError, } from 'kuzzle'; -import { Sensor } from '../models'; -import { Decoder } from '../decoders'; +import { + SensorBulkBuildedContent, + SensorBulkContent, + SensorMAttachementContent, + SensorMRequestContent +} from '../types'; +import { Decoder } from '../decoders'; +import { Sensor } from '../models'; export class SensorService { - private config: JSONObject; - private context: PluginContext; - - get sdk(): EmbeddedSDK { - return this.context.accessors.sdk; - } + private config: JSONObject; + private context: PluginContext; - constructor (config: JSONObject, context: PluginContext) { - this.config = config; - this.context = context; - } + get sdk(): EmbeddedSDK { + return this.context.accessors.sdk; + } - async attachTenant (sensor: Sensor, tenantId: string) { - if (sensor._source.tenantId) { - throw new BadRequestError(`Sensor "${sensor._id}" is already attached to a tenant`); + constructor(config: JSONObject, context: PluginContext) { + this.config = config; + this.context = context; } - const { result: tenantExists } = await this.sdk.query({ - controller: 'device-manager/engine', - action: 'exists', - index: tenantId, - }); + async mAttach (sensors: Sensor[], bulkData: SensorBulkContent[], { strict }): Promise { + const attachedSensors = sensors.filter(sensor => sensor._source.tenantId); + + if (strict && attachedSensors.length > 0) { + const ids = attachedSensors.map(sensor => sensor._id).join(',') + throw new BadRequestError(`These sensors "${ids}" are already attached to a tenant`); + } + + const documents = this.buildBulkSensors(bulkData); + const results = { + errors: [], + successes: [], + }; + + for (let i = 0; i < documents.length; i++) { + const document = documents[i]; + const tenantExists = await this.tenantExists(document.tenantId); + + if (strict && ! tenantExists) { + throw new BadRequestError(`Tenant "${document.tenantId}" does not have a device-manager engine`); + } + else if (! strict && ! tenantExists) { + results.errors.push(`Tenant "${document.tenantId}" does not have a device-manager engine`) + continue; + } + + const sensorDocuments = this.formatSensorsContent(sensors, document); + + const { errors, successes } = await this.writeToDatabase( + sensorDocuments, + async (sensorDocuments: SensorMRequestContent[]): Promise => { + const updated = await this.sdk.document.mUpdate( + this.config.adminIndex, + 'sensors', + sensorDocuments); + + await this.sdk.document.mCreate( + document.tenantId, + 'sensors', + sensorDocuments); + + return { + successes: results.successes.concat(updated.successes), + errors: results.errors.concat(updated.errors) + } + }); + + results.successes.concat(successes); + results.errors.concat(errors); + } + + return results; + } - if (!tenantExists) { - throw new BadRequestError(`Tenant "${tenantId}" does not have a device-manager engine`); + async detach (sensor: Sensor) { + if (! sensor._source.tenantId) { + throw new BadRequestError(`Sensor "${sensor._id}" is not attached to a tenant`); + } + + if (sensor._source.assetId) { + throw new BadRequestError(`Sensor "${sensor._id}" is still linked to an asset`); + } + + await this.sdk.document.delete( + sensor._source.tenantId, + 'sensors', + sensor._id); + + await this.sdk.document.update( + this.config.adminIndex, + 'sensors', + sensor._id, + { tenantId: null }); } - sensor._source.tenantId = tenantId; - await this.sdk.document.update( - this.config.adminIndex, - 'sensors', - sensor._id, - sensor._source); + async linkAsset (sensor: Sensor, assetId: string, decoders: Map) { + if (!sensor._source.tenantId) { + throw new BadRequestError(`Sensor "${sensor._id}" is not attached to a tenant`); + } + + const assetExists = await this.sdk.document.exists( + sensor._source.tenantId, + 'assets', + assetId); + + if (!assetExists) { + throw new BadRequestError(`Asset "${assetId}" does not exists`); + } + + await this.sdk.document.update( + this.config.adminIndex, + 'sensors', + sensor._id, + { assetId }); + + await this.sdk.document.update( + sensor._source.tenantId, + 'sensors', + sensor._id, + { assetId }); + + const decoder = decoders.get(sensor._source.model); + + const assetMeasures = await decoder.copyToAsset(sensor); + + await this.sdk.document.update( + sensor._source.tenantId, + 'assets', + assetId, + { measures: assetMeasures }); + } + + async unlink (sensor: Sensor) { + if (! sensor._source.assetId) { + throw new BadRequestError(`Sensor "${sensor._id}" is not linked to an asset`); + } + + await this.sdk.document.update( + this.config.adminIndex, + 'sensors', + sensor._id, + { assetId: null }); + + await this.sdk.document.update( + sensor._source.tenantId, + 'sensors', + sensor._id, + { assetId: null }); + + // @todo only remove the measures coming from the unlinked sensor + await this.sdk.document.update( + sensor._source.tenantId, + 'assets', + sensor._source.assetId, + { measures: null }); + } - await this.sdk.document.create( - tenantId, - 'sensors', - sensor._source, - sensor._id); - } + private async tenantExists (tenantId: string) { + const { result: tenantExists } = await this.sdk.query({ + controller: 'device-manager/engine', + action: 'exists', + index: tenantId, + }); - async detach (sensor: Sensor) { - if (!sensor._source.tenantId) { - throw new BadRequestError(`Sensor "${sensor._id}" is not attached to a tenant`); + return tenantExists; } - if (sensor._source.assetId) { - throw new BadRequestError(`Sensor "${sensor._id}" is still linked to an asset`); + private buildBulkSensors (bulkData: SensorBulkContent[]): SensorBulkBuildedContent[] { + const documents: SensorBulkBuildedContent[] = []; + + for (let i = 0; i < bulkData.length; i++) { + const { tenantId, sensorId } = bulkData[i]; + const document = documents.find(doc => doc.tenantId === tenantId); + + if (document) { + document.sensorIds.push(sensorId); + } + else { + documents.push({ tenantId, sensorIds: [sensorId] }) + } + } + return documents; } - await this.sdk.document.delete( - sensor._source.tenantId, - 'sensors', - sensor._id); + private formatSensorsContent (sensors: Sensor[], document: SensorBulkBuildedContent): SensorMRequestContent[] { + const sensorsContent = sensors.filter(sensor => document.sensorIds.includes(sensor._id)); + const sensorsDocuments = sensorsContent.map(sensor => { + sensor._source.tenantId = document.tenantId; + return { _id: sensor._id, body: sensor._source } + }); - await this.sdk.document.update( - this.config.adminIndex, - 'sensors', - sensor._id, - { tenantId: null }); - } + return sensorsDocuments; + } + private async writeToDatabase (sensorDocuments: SensorMRequestContent[], writer: (sensorDocuments: SensorMRequestContent[]) => Promise) { + const results = { + errors: [], + successes: [], + } - async linkAsset (sensor: Sensor, assetId: string, decoders: Map) { - if (!sensor._source.tenantId) { - throw new BadRequestError(`Sensor "${sensor._id}" is not attached to a tenant`); - } + const limit = global.kuzzle.config.limits.documentsWriteCount; - const assetExists = await this.sdk.document.exists( - sensor._source.tenantId, - 'assets', - assetId); + if (sensorDocuments.length <= limit) { + const { successes, errors } = await writer(sensorDocuments); + results.successes.push(successes); + results.errors.push(errors); - if (!assetExists) { - throw new BadRequestError(`Asset "${assetId}" does not exists`); - } + return results; + } - await this.sdk.document.update( - this.config.adminIndex, - 'sensors', - sensor._id, - { assetId }); + const writeMany = async (start: number, end: number) => { + const sensors = sensorDocuments.slice(start, end); + const { successes, errors } = await writer(sensors); - await this.sdk.document.update( - sensor._source.tenantId, - 'sensors', - sensor._id, - { assetId }); + results.successes.push(successes); + results.errors.push(errors); + } - const decoder = decoders.get(sensor._source.model); + let offset = 0; + let offsetLimit = limit; + let done = false; - const assetMeasures = await decoder.copyToAsset(sensor); + while (! done) { + await writeMany(offset, offsetLimit) - await this.sdk.document.update( - sensor._source.tenantId, - 'assets', - assetId, - { measures: assetMeasures }); - } + offset += limit; + offsetLimit += limit; - async unlink (sensor: Sensor) { - if (!sensor._source.assetId) { - throw new BadRequestError(`Sensor "${sensor._id}" is not linked to an asset`); - } + if (offsetLimit >= sensorDocuments.length) { + done = true; + await writeMany(offset, sensorDocuments.length); + } + } - await this.sdk.document.update( - this.config.adminIndex, - 'sensors', - sensor._id, - { assetId: null }); - - await this.sdk.document.update( - sensor._source.tenantId, - 'sensors', - sensor._id, - { assetId: null }); - - // @todo only remove the measures coming from the unlinked sensor - await this.sdk.document.update( - sensor._source.tenantId, - 'assets', - sensor._source.assetId, - { measures: null }); - } + return results; + } } diff --git a/lib/types/SensorContent.ts b/lib/types/SensorContent.ts index cf60bf93..eabe9416 100644 --- a/lib/types/SensorContent.ts +++ b/lib/types/SensorContent.ts @@ -51,3 +51,23 @@ export type SensorContent = { updatedAt?: number | null } } + +export type SensorBulkContent = { + tenantId: string; + sensorId: string; +} + +export type SensorBulkBuildedContent = { + tenantId: string; + sensorIds: string[]; +} + +export type SensorMAttachementContent = { + errors: JSONObject[]; + successes: JSONObject[]; +} + +export type SensorMRequestContent = { + _id: string; + body: JSONObject; +} diff --git a/package-lock.json b/package-lock.json index 7f821b6c..d6298867 100644 --- a/package-lock.json +++ b/package-lock.json @@ -747,8 +747,7 @@ "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, "boost-geospatial-index": { "version": "1.1.0", @@ -1395,6 +1394,16 @@ "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "dev": true }, + "csvtojson": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/csvtojson/-/csvtojson-2.0.10.tgz", + "integrity": "sha512-lUWFxGKyhraKCW8Qghz6Z0f2l/PqB1W3AO0HKJzGIQ5JRSlR651ekJDiGJbBT4sRNNv5ddnSGVEnsxP9XRCVpQ==", + "requires": { + "bluebird": "^3.5.1", + "lodash": "^4.17.3", + "strip-bom": "^2.0.0" + } + }, "cucumber": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cucumber/-/cucumber-6.0.5.tgz", @@ -3403,6 +3412,11 @@ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "dev": true }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + }, "is-yarn-global": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", @@ -4161,8 +4175,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.defaults": { "version": "4.2.0", @@ -6664,6 +6677,14 @@ "ansi-regex": "^3.0.0" } }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "requires": { + "is-utf8": "^0.2.0" + } + }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", diff --git a/package.json b/package.json index 3198e78d..cf229776 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ }, "license": "Apache-2.0", "dependencies": { + "csvtojson": "~2.0.10", "uuid": "~8.3.2" }, "devDependencies": {