Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix into 2820-add_item_scopeDays
gitea/salix/pipeline/head This commit looks good Details

This commit is contained in:
jorgebl 2021-02-25 11:02:51 +01:00
commit 4cedd53bce
20 changed files with 444 additions and 32586 deletions

View File

@ -1,11 +1,51 @@
const fs = require('fs-extra');
const sharp = require('sharp');
const path = require('path');
const readChunk = require('read-chunk');
const imageType = require('image-type');
const bmp = require('bmp-js');
module.exports = Self => {
require('../methods/image/download')(Self);
require('../methods/image/upload')(Self);
// Function extracted from jimp package (utils)
function scan(image, x, y, w, h, f) {
// round input
x = Math.round(x);
y = Math.round(y);
w = Math.round(w);
h = Math.round(h);
for (let _y = y; _y < y + h; _y++) {
for (let _x = x; _x < x + w; _x++) {
const idx = (image.bitmap.width * _y + _x) << 2;
f.call(image, _x, _y, idx);
}
}
return image;
}
// Function extracted from jimp package (type-bmp)
function fromAGBR(bitmap) {
return scan({bitmap}, 0, 0, bitmap.width, bitmap.height, function(
x,
y,
index
) {
const alpha = this.bitmap.data[index + 0];
const blue = this.bitmap.data[index + 1];
const green = this.bitmap.data[index + 2];
const red = this.bitmap.data[index + 3];
this.bitmap.data[index + 0] = red;
this.bitmap.data[index + 1] = green;
this.bitmap.data[index + 2] = blue;
this.bitmap.data[index + 3] = bitmap.is_with_alpha ? alpha : 0xff;
}).bitmap;
}
Self.registerImage = async(collectionName, srcFilePath, fileName, entityId) => {
const models = Self.app.models;
const tx = await Self.beginTransaction({});
@ -48,13 +88,31 @@ module.exports = Self => {
const dstDir = path.join(collectionDir, 'full');
const dstFile = path.join(dstDir, file);
const buffer = readChunk.sync(srcFilePath, 0, 12);
const type = imageType(buffer);
let sharpOptions;
let imgSrc = srcFilePath;
if (type.mime == 'image/bmp') {
const bmpBuffer = fs.readFileSync(srcFilePath);
const bmpData = fromAGBR(bmp.decode(bmpBuffer));
imgSrc = bmpData.data;
sharpOptions = {
raw: {
width: bmpData.width,
height: bmpData.height,
channels: 4
}
};
}
const resizeOpts = {
withoutEnlargement: true,
fit: 'inside'
};
await fs.mkdir(dstDir, {recursive: true});
await sharp(srcFilePath, {failOnError: false})
await sharp(imgSrc, sharpOptions)
.resize(collection.maxWidth, collection.maxHeight, resizeOpts)
.png()
.toFile(dstFile);
@ -69,7 +127,7 @@ module.exports = Self => {
};
await fs.mkdir(dstDir, {recursive: true});
await sharp(srcFilePath, {failOnError: false})
await sharp(imgSrc, sharpOptions)
.resize(size.width, size.height, resizeOpts)
.png()
.toFile(dstFile);

View File

@ -78,6 +78,8 @@ module.exports = Self => {
return {'ic.id': value};
case 'salesPersonFk':
return {'it.workerFk': value};
case 'code':
return {'it.code': value};
case 'typeFk':
return {'i.typeFk': value};
case 'active':
@ -103,6 +105,7 @@ module.exports = Self => {
i.id AS itemFk,
i.size,
i.density,
it.code,
i.typeFk,
i.family,
i.isActive,

View File

@ -37,8 +37,8 @@
<vn-th field="quantity">Quantity</vn-th>
<vn-th field="description" style="text-align: center">Description</vn-th>
<vn-th field="size">Size</vn-th>
<vn-th field="tags" style="text-align: center">Tags</vn-th>
<vn-th field="type">Type</vn-th>
<vn-th field="name" style="text-align: center">Tags</vn-th>
<vn-th field="code">Type</vn-th>
<vn-th field="intrastat">Intrastat</vn-th>
<vn-th field="origin">Origin</vn-th>
<vn-th field="density">Density</vn-th>
@ -109,7 +109,7 @@
</vn-fetched-tags>
</vn-td>
<vn-td shrink title="{{::buy.type}}">
{{::buy.type}}
{{::buy.code}}
</vn-td>
<vn-td shrink title="{{::item.intrastat}}">
{{::buy.intrastat}}

View File

@ -46,13 +46,8 @@ module.exports = Self => {
if (!image) return;
const srcFile = image.url.split('/').pop();
const dotIndex = srcFile.lastIndexOf('.');
let fileName = srcFile.substring(0, dotIndex);
if (dotIndex == -1)
fileName = srcFile;
const srcFile = image.url;
const fileName = srcFile.replace(/\.|\/|:|\?|\\|=|%/g, '');
const file = `${fileName}.png`;
const filePath = path.join(tempPath, file);

View File

@ -44,6 +44,10 @@ module.exports = Self => {
arg: 'description',
type: 'String',
description: 'The item description',
}, {
arg: 'stemMultiplier',
type: 'Integer',
description: 'The item multiplier',
}
],
returns: {
@ -80,16 +84,22 @@ module.exports = Self => {
: {or: [{'i.name': {like: `%${value}%`}}, codeWhere]};
case 'id':
return {'i.id': value};
case 'description':
return {'i.description': {like: `%${value}%`}};
case 'categoryFk':
return {'ic.id': value};
case 'salesPersonFk':
return {'t.workerFk': value};
case 'typeFk':
return {'i.typeFk': value};
case 'isActive':
return {'i.isActive': value};
case 'multiplier':
return {'i.stemMultiplier': value};
case 'typeFk':
return {'i.typeFk': value};
case 'category':
return {'ic.name': value};
case 'salesPersonFk':
return {'it.workerFk': value};
case 'origin':
return {'ori.code': value};
case 'niche':
return {'ip.code': value};
case 'intrastat':
return {'intr.description': value};
}
});
filter = mergeFilters(filter, {where});
@ -98,7 +108,8 @@ module.exports = Self => {
let stmt;
stmt = new ParameterizedSQL(
`SELECT i.id,
`SELECT
i.id,
i.image,
i.name,
i.description,
@ -111,29 +122,30 @@ module.exports = Self => {
i.tag10, i.value10,
i.subName,
i.isActive,
t.name type,
t.workerFk buyerFk,
u.name userName,
intr.description AS intrastat,
i.stems,
ori.code AS origin,
ic.name AS category,
i.density,
i.stemMultiplier,
i.typeFk,
it.name AS typeName,
it.workerFk AS buyerFk,
u.name AS userName,
ori.code AS origin,
ic.name AS category,
intr.description AS intrastat,
b.grouping,
b.packing,
itn.code AS niche, @visibleCalc
ip.code AS niche, @visibleCalc
FROM item i
LEFT JOIN itemType t ON t.id = i.typeFk
LEFT JOIN itemCategory ic ON ic.id = t.categoryFk
LEFT JOIN worker w ON w.id = t.workerFk
LEFT JOIN itemType it ON it.id = i.typeFk
LEFT JOIN itemCategory ic ON ic.id = it.categoryFk
LEFT JOIN worker w ON w.id = it.workerFk
LEFT JOIN account.user u ON u.id = w.userFk
LEFT JOIN intrastat intr ON intr.id = i.intrastatFk
LEFT JOIN producer pr ON pr.id = i.producerFk
LEFT JOIN origin ori ON ori.id = i.originFk
LEFT JOIN cache.last_buy lb ON lb.item_id = i.id AND lb.warehouse_id = t.warehouseFk
LEFT JOIN cache.last_buy lb ON lb.item_id = i.id AND lb.warehouse_id = it.warehouseFk
LEFT JOIN vn.buy b ON b.id = lb.buy_id
LEFT JOIN itemPlacement itn ON itn.itemFk = i.id AND itn.warehouseFk = t.warehouseFk`
LEFT JOIN itemPlacement ip ON ip.itemFk = i.id AND ip.warehouseFk = it.warehouseFk`
);
if (ctx.args.tags) {

View File

@ -113,6 +113,12 @@
ng-model="$ctrl.item.stems"
rule>
</vn-input-number>
<vn-input-number
vn-one
min="0"
label="Multiplier"
ng-model="$ctrl.item.stemMultiplier">
</vn-input-number>
</vn-horizontal>
<vn-horizontal>
<vn-input-number

View File

@ -9,4 +9,5 @@ Price in kg: Precio en kg
New intrastat: Nuevo intrastat
Identifier: Identificador
Fragile: Frágil
Is shown at website, app that this item cannot travel (wreath, palms, ...): Se muestra en la web, app que este artículo no puede viajar (coronas, palmas, ...)
Is shown at website, app that this item cannot travel (wreath, palms, ...): Se muestra en la web, app que este artículo no puede viajar (coronas, palmas, ...)
Multiplier: Multiplicador

View File

@ -5,107 +5,107 @@
model="model"
class="vn-w-xl vn-mb-xl">
<vn-card>
<vn-table
model="model"
show-fields="$ctrl.showFields"
vn-smart-table="itemIndex">
<vn-thead>
<vn-tr>
<vn-th shrink></vn-th>
<vn-th field="id" shrink>Id</vn-th>
<vn-th field="grouping" shrink>Grouping</vn-th>
<vn-th field="packing" shrink>Packing</vn-th>
<vn-th field="description">Description</vn-th>
<vn-th field="stems" shrink>Stems</vn-th>
<vn-th field="size" shrink>Size</vn-th>
<vn-th field="niche" shrink>Niche</vn-th>
<vn-th field="type" shrink>Type</vn-th>
<vn-th field="category" shrink>Category</vn-th>
<vn-th field="intrastat" shrink>Intrastat</vn-th>
<vn-th field="origin" shrink>Origin</vn-th>
<vn-th field="salesperson" shrink>Buyer</vn-th>
<vn-th field="density" shrink>Density</vn-th>
<vn-th field="stemMultiplier" shrink>Multiplier</vn-th>
<vn-th field="active" shrink>Active</vn-th>
<vn-th></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<a ng-repeat="item in model.data"
class="clickable vn-tr search-result"
ui-sref="item.card.summary({id: item.id})">
<vn-td shrink>
<img
ng-src="{{::$root.imagePath('catalog', '50x50', item.id)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', item.id)}}"
vn-click-stop
on-error-src/>
</vn-td>
<vn-td shrink>
<span
vn-click-stop="itemDescriptor.show($event, item.id)"
class="link">
{{::item.id}}
</span>
</vn-td>
<vn-td shrink>{{::item.grouping | dashIfEmpty}}</vn-td>
<vn-td shrink>{{::item.packing | dashIfEmpty}}</vn-td>
<vn-td vn-fetched-tags>
<vn-one title="{{::item.name}}">{{::item.name}}</vn-one>
<vn-one ng-if="::item.subName">
<h3 title="{{::item.subName}}">{{::item.subName}}</h3>
</vn-one>
<vn-fetched-tags
max-length="6"
item="item"
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td shrink>{{::item.stems}}</vn-td>
<vn-td shrink>{{::item.size}}</vn-td>
<vn-td shrink>{{::item.niche}}</vn-td>
<vn-td shrink title="{{::item.type}}">
{{::item.type}}
</vn-td>
<vn-td shrink title="{{::item.category}}">
{{::item.category}}
</vn-td>
<vn-td shrink title="{{::item.intrastat}}">
{{::item.intrastat}}
</vn-td>
<vn-td shrink>{{::item.origin}}</vn-td>
<vn-td shrink title="{{::item.userName}}">
<span
class="link"
vn-click-stop="workerDescriptor.show($event, item.buyerFk)">
{{::item.userName}}
</span>
</vn-td>
<vn-td shrink>{{::item.density}}</vn-td>
<vn-td shrink >{{::item.stemMultiplier}}</vn-td>
<vn-td shrink>
<vn-check
disabled="true"
ng-model="::item.isActive">
</vn-check>
</vn-td>
<vn-td shrink>
<vn-horizontal class="buttons">
<vn-icon-button
vn-click-stop="clone.show(item.id)"
vn-tooltip="Clone"
icon="icon-clone">
</vn-icon-button>
<vn-icon-button
vn-click-stop="$ctrl.preview(item)"
vn-tooltip="Preview"
icon="preview">
</vn-icon-button>
</vn-horizontal>
</vn-td>
</a>
</vn-tbody>
</vn-table>
<vn-table
model="model"
show-fields="$ctrl.showFields"
vn-smart-table="itemIndex">
<vn-thead>
<vn-tr>
<vn-th shrink></vn-th>
<vn-th field="id" shrink>Id</vn-th>
<vn-th field="grouping" shrink>Grouping</vn-th>
<vn-th field="packing" shrink>Packing</vn-th>
<vn-th field="name">Description</vn-th>
<vn-th field="stems" shrink>Stems</vn-th>
<vn-th field="size" shrink>Size</vn-th>
<vn-th field="niche" shrink>Niche</vn-th>
<vn-th field="typeFk" shrink>Type</vn-th>
<vn-th field="category" shrink>Category</vn-th>
<vn-th field="intrastat" shrink>Intrastat</vn-th>
<vn-th field="origin" shrink>Origin</vn-th>
<vn-th field="salesperson" shrink>Buyer</vn-th>
<vn-th field="density" shrink>Density</vn-th>
<vn-th field="stemMultiplier" shrink>Multiplier</vn-th>
<vn-th field="active" shrink>Active</vn-th>
<vn-th></vn-th>
</vn-tr>
</vn-thead>
<vn-tbody>
<a ng-repeat="item in model.data"
class="clickable vn-tr search-result"
ui-sref="item.card.summary({id: item.id})">
<vn-td shrink>
<img
ng-src="{{::$root.imagePath('catalog', '50x50', item.id)}}"
zoom-image="{{::$root.imagePath('catalog', '1600x900', item.id)}}"
vn-click-stop
on-error-src/>
</vn-td>
<vn-td shrink>
<span
vn-click-stop="itemDescriptor.show($event, item.id)"
class="link">
{{::item.id}}
</span>
</vn-td>
<vn-td shrink>{{::item.grouping | dashIfEmpty}}</vn-td>
<vn-td shrink>{{::item.packing | dashIfEmpty}}</vn-td>
<vn-td vn-fetched-tags>
<vn-one title="{{::item.name}}">{{::item.name}}</vn-one>
<vn-one ng-if="::item.subName">
<h3 title="{{::item.subName}}">{{::item.subName}}</h3>
</vn-one>
<vn-fetched-tags
max-length="6"
item="item"
tabindex="-1">
</vn-fetched-tags>
</vn-td>
<vn-td shrink>{{::item.stems}}</vn-td>
<vn-td shrink>{{::item.size}}</vn-td>
<vn-td shrink>{{::item.niche}}</vn-td>
<vn-td shrink title="{{::item.typeName}}">
{{::item.typeName}}
</vn-td>
<vn-td shrink title="{{::item.category}}">
{{::item.category}}
</vn-td>
<vn-td shrink title="{{::item.intrastat}}">
{{::item.intrastat}}
</vn-td>
<vn-td shrink>{{::item.origin}}</vn-td>
<vn-td shrink title="{{::item.userName}}">
<span
class="link"
vn-click-stop="workerDescriptor.show($event, item.buyerFk)">
{{::item.userName}}
</span>
</vn-td>
<vn-td shrink>{{::item.density}}</vn-td>
<vn-td shrink >{{::item.stemMultiplier}}</vn-td>
<vn-td shrink>
<vn-check
disabled="true"
ng-model="::item.isActive">
</vn-check>
</vn-td>
<vn-td shrink>
<vn-horizontal class="buttons">
<vn-icon-button
vn-click-stop="clone.show(item.id)"
vn-tooltip="Clone"
icon="icon-clone">
</vn-icon-button>
<vn-icon-button
vn-click-stop="$ctrl.preview(item)"
vn-tooltip="Preview"
icon="preview">
</vn-icon-button>
</vn-horizontal>
</vn-td>
</a>
</vn-tbody>
</vn-table>
</vn-card>
</vn-data-viewer>
<a ui-sref="item.create" vn-tooltip="New item" vn-bind="+" fixed-bottom-right>
@ -127,4 +127,31 @@
<vn-item-summary
item="$ctrl.itemSelected">
</vn-item-summary>
</vn-popup>
</vn-popup>
<vn-contextmenu
vn-id="contextmenu"
targets="['vn-data-viewer']"
model="model"
expr-builder="$ctrl.exprBuilder(param, value)">
<slot-menu>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.filterBySelection()">
Filter by selection
</vn-item>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.excludeSelection()">
Exclude selection
</vn-item>
<vn-item translate
ng-if="contextmenu.isFilterAllowed()"
ng-click="contextmenu.removeFilter()">
Remove filter
</vn-item>
<vn-item translate
ng-click="contextmenu.removeAllFilters()">
Remove all filters
</vn-item>
</slot-menu>
</vn-contextmenu>

View File

@ -11,6 +11,36 @@ class Controller extends Section {
};
}
exprBuilder(param, value) {
switch (param) {
case 'category':
return {'ic.name': value};
case 'salesPersonFk':
return {'it.workerFk': value};
case 'grouping':
return {'b.grouping': value};
case 'packing':
return {'b.packing': value};
case 'origin':
return {'ori.code': value};
case 'niche':
return {'ip.code': value};
case 'typeFk':
return {'i.typeFk': value};
case 'intrastat':
return {'intr.description': value};
case 'id':
case 'size':
case 'name':
case 'subname':
case 'isActive':
case 'density':
case 'stemMultiplier':
case 'stems':
return {[`i.${param}`]: value};
}
}
onCloneAccept(itemFk) {
return this.$http.post(`Items/${itemFk}/clone`)
.then(res => {

View File

@ -55,6 +55,9 @@
<vn-label-value label="stems"
value="{{$ctrl.summary.item.stems}}">
</vn-label-value>
<vn-label-value label="Multiplier"
value="{{$ctrl.summary.item.stemMultiplier}}">
</vn-label-value>
<vn-label-value label="Buyer">
<span
ng-click="workerDescriptor.show($event, $ctrl.summary.item.itemType.worker.userFk)"

View File

@ -1,3 +1,4 @@
const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
module.exports = Self => {
Self.remoteMethod('getAverageDays', {
description: 'Returns the average days duration and the two warehouses of the travel.',
@ -8,7 +9,7 @@ module.exports = Self => {
required: true
}],
returns: {
type: 'number',
type: 'object',
root: true
},
http: {
@ -18,15 +19,48 @@ module.exports = Self => {
});
Self.getAverageDays = async agencyModeFk => {
const query = `
SELECT t.id, t.warehouseInFk, t.warehouseOutFk,
(SELECT ROUND(AVG(DATEDIFF(t.landed, t.shipped )))
FROM travel t
WHERE t.agencyFk = ? LIMIT 50) AS dayDuration
FROM travel t
WHERE t.agencyFk = ? ORDER BY t.id DESC LIMIT 1;`;
const conn = Self.dataSource.connector;
let stmts = [];
stmts.push('DROP TEMPORARY TABLE IF EXISTS tmp.travel');
stmt = new ParameterizedSQL(`
CREATE TEMPORARY TABLE tmp.travel (
SELECT
t.id,
t.warehouseInFk,
t.warehouseOutFk,
t.landed,
t.shipped,
t.agencyFk
FROM travel t
WHERE t.agencyFk = ? LIMIT 50)`, [agencyModeFk]);
stmts.push(stmt);
stmt = new ParameterizedSQL(`
SELECT
t.id,
t.warehouseInFk,
t.warehouseOutFk,
(SELECT ROUND(AVG(DATEDIFF(t.landed, t.shipped )))
FROM tmp.travel t
WHERE t.agencyFk
ORDER BY id DESC LIMIT 50) AS dayDuration
FROM tmp.travel t
WHERE t.agencyFk
ORDER BY t.id DESC LIMIT 1`);
const avgDaysIndex = stmts.push(stmt) - 1;
stmts.push(
`DROP TEMPORARY TABLE
tmp.travel`);
const sql = ParameterizedSQL.join(stmts, ';');
const result = await conn.executeStmt(sql);
const [avgDays] = result[avgDaysIndex];
const [avgDays] = await Self.rawSql(query, [agencyModeFk, agencyModeFk]);
return avgDays;
};
};

View File

@ -22,6 +22,9 @@ class Controller extends Section {
agencyModeFk: this.travel.agencyModeFk
};
this.$http.get(query, {params}).then(res => {
if (!res.data)
return;
const landed = new Date(value);
const futureDate = landed.getDate() + res.data.dayDuration;
landed.setDate(futureDate);

View File

@ -51,14 +51,29 @@ describe('Travel Component vnTravelCreate', () => {
expect(controller.travel.warehouseOutFk).toBeUndefined();
});
it(`should do nothing if there's no response data.`, () => {
controller.travel = {agencyModeFk: 4};
const tomorrow = new Date();
const query = `travels/getAverageDays?agencyModeFk=${controller.travel.agencyModeFk}`;
$httpBackend.expectGET(query).respond(undefined);
controller.onShippedChange(tomorrow);
$httpBackend.flush();
expect(controller.travel.warehouseInFk).toBeUndefined();
expect(controller.travel.warehouseOutFk).toBeUndefined();
expect(controller.travel.dayDuration).toBeUndefined();
});
it(`should fill the fields when it's selected a date and agency.`, () => {
controller.travel = {agencyModeFk: 1};
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setDate(tomorrow.getDate() + 9);
const expectedResponse = {
dayDuration: 2,
warehouseInFk: 1,
warehouseOutFk: 2
id: 8,
dayDuration: 9,
warehouseInFk: 5,
warehouseOutFk: 1
};
const query = `travels/getAverageDays?agencyModeFk=${controller.travel.agencyModeFk}`;

View File

@ -42,6 +42,11 @@
"model": "Account",
"foreignKey": "userFk"
},
"boss": {
"type": "belongsTo",
"model": "Account",
"foreignKey": "bossFk"
},
"client": {
"type": "belongsTo",
"model": "Client",

View File

@ -29,6 +29,15 @@
ng-model="$ctrl.worker.phone"
rule>
</vn-textfield>
<vn-autocomplete
disabled="false"
ng-model="$ctrl.worker.bossFk"
url="Clients/activeWorkersWithRole"
show-field="nickname"
search-function="{firstName: $search}"
where="{role: 'employee'}"
label="Boss">
</vn-autocomplete>
</vn-horizontal>
</vn-vertical>
</vn-card>

View File

@ -7,6 +7,7 @@ Extension: Extensión
Fiscal identifier: NIF
Go to client: Ir al cliente
Last name: Apellidos
Boss: Jefe
Log: Historial
Private Branch Exchange: Centralita
Role: Rol

View File

@ -30,6 +30,14 @@
<vn-label-value label="Department"
value="{{worker.department.department.name}}">
</vn-label-value>
<vn-label-value
label="Boss">
<span
ng-click="workerDescriptor.show($event, worker.boss.id)"
class="link">
{{::worker.boss.nickname}}
</span>
</vn-label-value>
<vn-label-value label="Phone"
value="{{worker.phone}}">
</vn-label-value>
@ -50,4 +58,7 @@
</vn-label-value>
</vn-one>
</vn-horizontal>
</vn-card>
</vn-card>
<vn-worker-descriptor-popover
vn-id="workerDescriptor">
</vn-worker-descriptor-popover>

View File

@ -11,8 +11,8 @@ class Controller extends Summary {
this.$.worker = null;
if (!value) return;
let query = `Workers/${value.id}`;
let filter = {
const query = `Workers/${value.id}`;
const filter = {
include: [
{
relation: 'user',
@ -31,13 +31,20 @@ class Controller extends Summary {
}
}]
}
}, {
},
{
relation: 'client',
scope: {fields: ['fi']}
}, {
},
{
relation: 'boss',
scope: {fields: ['id', 'nickname']}
},
{
relation: 'sip',
scope: {fields: ['extension']}
}, {
},
{
relation: 'department',
scope: {
include: {

32497
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,10 +12,12 @@
"node": ">=12"
},
"dependencies": {
"bmp-js": "^0.1.0",
"compression": "^1.7.3",
"fs-extra": "^5.0.0",
"helmet": "^3.21.2",
"i18n": "^0.8.4",
"image-type": "^4.1.0",
"imap": "^0.8.19",
"ldapjs": "^2.2.0",
"loopback": "^3.26.0",
@ -30,6 +32,7 @@
"node-ssh": "^11.0.0",
"object-diff": "0.0.4",
"object.pick": "^1.3.0",
"read-chunk": "^3.2.0",
"request": "^2.88.0",
"request-promise-native": "^1.0.8",
"require-yaml": "0.0.1",