iotagent-node-lib 3.3.0 → 3.4.1
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/CHANGES_NEXT_RELEASE +1 -0
- package/README.md +10 -11
- package/doc/README.md +16 -0
- package/doc/admin.md +565 -0
- package/doc/api.md +9 -5
- package/doc/deprecated.md +16 -14
- package/doc/{architecture.md → devel/architecture.md} +3 -3
- package/doc/{Contribution.md → devel/contribution-guidelines.md} +43 -35
- package/doc/devel/development.md +1879 -0
- package/doc/{northboundinteractions.md → devel/northboundinteractions.md} +18 -33
- package/doc/index.md +3 -5
- package/docker/Mosquitto/README.md +1 -0
- package/lib/commonConfig.js +0 -5
- package/lib/fiware-iotagent-lib.js +1 -1
- package/lib/jexlTranformsMap.js +2 -1
- package/lib/request-shim.js +2 -2
- package/lib/services/commands/commandService.js +1 -1
- package/lib/services/common/genericMiddleware.js +1 -1
- package/lib/services/devices/deviceRegistryMemory.js +2 -2
- package/lib/services/devices/deviceRegistryMongoDB.js +22 -9
- package/lib/services/devices/deviceService.js +36 -30
- package/lib/services/devices/devices-NGSI-LD.js +14 -2
- package/lib/services/devices/devices-NGSI-mixed.js +0 -2
- package/lib/services/devices/devices-NGSI-v2.js +22 -100
- package/lib/services/groups/groupService.js +1 -1
- package/lib/services/ngsi/entities-NGSI-v2.js +14 -27
- package/lib/services/northBound/deviceProvisioningServer.js +14 -5
- package/mkdocs.yml +6 -11
- package/package.json +3 -3
- package/scripts/legacy_expression_tool/README.md +56 -38
- package/test/unit/general/contextBrokerKeystoneSecurityAccess-test.js +2 -2
- package/test/unit/mongodb/mongodb-registry-test.js +1 -1
- package/test/unit/ngsi-ld/general/contextBrokerOAuthSecurityAccess-test.js +66 -65
- package/test/unit/ngsi-ld/general/https-support-test.js +1 -1
- package/test/unit/ngsi-ld/lazyAndCommands/command-test.js +8 -7
- package/test/unit/ngsi-ld/lazyAndCommands/polling-commands-test.js +12 -11
- package/test/unit/ngsi-ld/ngsiService/subscriptions-test.js +41 -39
- package/test/unit/ngsi-ld/provisioning/device-provisioning-api_test.js +122 -122
- package/test/unit/ngsi-ld/provisioning/device-registration_test.js +28 -28
- package/test/unit/ngsi-ld/provisioning/device-update-registration_test.js +18 -17
- package/test/unit/ngsi-ld/provisioning/singleConfigurationMode-test.js +7 -7
- package/test/unit/ngsi-ld/provisioning/updateProvisionedDevices-test.js +8 -7
- package/test/unit/ngsiv2/examples/contextRequests/updateContext6.json +2 -0
- package/test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin1.json +0 -12
- package/test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin11.json +0 -4
- package/test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin12.json +1 -5
- package/test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin2.json +0 -12
- package/test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin29.json +0 -12
- package/test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin3.json +0 -4
- package/test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin41.json +0 -10
- package/test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin5.json +0 -4
- package/test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin6.json +0 -4
- package/test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin7.json +0 -4
- package/test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin8.json +0 -12
- package/test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin9.json +0 -4
- package/test/unit/ngsiv2/examples/contextRequests/updateContextMultientityPlugin25.json +1 -5
- package/test/unit/ngsiv2/expressions/jexlBasedTransformations-test.js +13 -12
- package/test/unit/ngsiv2/general/contextBrokerOAuthSecurityAccess-test.js +2 -2
- package/test/unit/ngsiv2/general/https-support-test.js +1 -1
- package/test/unit/ngsiv2/ngsiService/active-devices-test.js +3 -8
- package/test/unit/ngsiv2/ngsiService/subscriptions-test.js +10 -10
- package/test/unit/ngsiv2/provisioning/device-provisioning-api_test.js +8 -103
- package/test/unit/ngsiv2/provisioning/device-registration_test.js +8 -6
- package/test/unit/ngsiv2/provisioning/device-update-registration_test.js +2 -1
- package/test/unit/ngsiv2/provisioning/updateProvisionedDevices-test.js +0 -1
- package/.nyc_output/33364de2-1199-4ec2-b33c-cae063ef8cc4.json +0 -1
- package/.nyc_output/processinfo/33364de2-1199-4ec2-b33c-cae063ef8cc4.json +0 -1
- package/.nyc_output/processinfo/index.json +0 -1
- package/doc/config-basic-example.js +0 -20
- package/doc/development.md +0 -285
- package/doc/howto.md +0 -641
- package/doc/installationguide.md +0 -365
- package/doc/operations.md +0 -127
- package/doc/usermanual.md +0 -900
- package/lib/plugins/bidirectionalData.js +0 -356
- package/test/unit/ngsi-ld/plugins/bidirectional-plugin_test.js +0 -697
- package/test/unit/ngsiv2/plugins/bidirectional-plugin_test.js +0 -536
- /package/doc/{NorthboundInteractions.postman_collection → devel/NorthboundInteractions.postman_collection} +0 -0
- /package/doc/{echo.js → devel/echo.js} +0 -0
- /package/doc/{finalResult.js → devel/finalResult.js} +0 -0
|
@@ -1,356 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2016 Telefonica Investigación y Desarrollo, S.A.U
|
|
3
|
-
*
|
|
4
|
-
* This file is part of fiware-iotagent-lib
|
|
5
|
-
*
|
|
6
|
-
* fiware-iotagent-lib is free software: you can redistribute it and/or
|
|
7
|
-
* modify it under the terms of the GNU Affero General Public License as
|
|
8
|
-
* published by the Free Software Foundation, either version 3 of the License,
|
|
9
|
-
* or (at your option) any later version.
|
|
10
|
-
*
|
|
11
|
-
* fiware-iotagent-lib is distributed in the hope that it will be useful,
|
|
12
|
-
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
-
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
14
|
-
* See the GNU Affero General Public License for more details.
|
|
15
|
-
*
|
|
16
|
-
* You should have received a copy of the GNU Affero General Public
|
|
17
|
-
* License along with fiware-iotagent-lib.
|
|
18
|
-
* If not, see http://www.gnu.org/licenses/.
|
|
19
|
-
*
|
|
20
|
-
* For those usages not covered by the GNU Affero General Public License
|
|
21
|
-
* please contact with::daniel.moranjimenez@telefonica.com
|
|
22
|
-
*
|
|
23
|
-
* Modified by: Daniel Calvo - ATOS Research & Innovation
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
const async = require('async');
|
|
27
|
-
const apply = async.apply;
|
|
28
|
-
const _ = require('underscore');
|
|
29
|
-
const parser = require('./expressionPlugin');
|
|
30
|
-
const logger = require('logops');
|
|
31
|
-
const config = require('../commonConfig');
|
|
32
|
-
const subscriptions = require('../services/ngsi/subscriptionService');
|
|
33
|
-
const deviceService = require('../services/devices/deviceService');
|
|
34
|
-
const context = {
|
|
35
|
-
op: 'IoTAgentNGSI.BidirectionalPlugin'
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Extract a list of all the bidirectional attributes (those containing reverse expressions) from a device object.
|
|
40
|
-
* If the device belongs to a Configuration Group, its attributes are also considered.
|
|
41
|
-
*
|
|
42
|
-
* @param {Object} device Device data object.
|
|
43
|
-
* @param {Object} group Configuration Group data object.
|
|
44
|
-
*/
|
|
45
|
-
function extractBidirectionalAttributes(device, group, callback) {
|
|
46
|
-
let attributeList;
|
|
47
|
-
|
|
48
|
-
function isBidirectional(item) {
|
|
49
|
-
return item.reverse;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (device.active) {
|
|
53
|
-
attributeList = device.active.filter(isBidirectional);
|
|
54
|
-
} else {
|
|
55
|
-
attributeList = [];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (group && group.attributes) {
|
|
59
|
-
attributeList = attributeList.concat(group.attributes.filter(isBidirectional));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
logger.debug(context, 'Extracting attribute list');
|
|
63
|
-
|
|
64
|
-
callback(null, attributeList);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Extract all the variables that exists in the collection of reverse attribute expressions of an attribute.
|
|
69
|
-
*
|
|
70
|
-
* @param {Object} item Attribute with a collection of reverse expressions.
|
|
71
|
-
* @return {Array} List of variables in all the collection of reverse expressions.
|
|
72
|
-
*/
|
|
73
|
-
function extractVariables(item) {
|
|
74
|
-
let variables;
|
|
75
|
-
|
|
76
|
-
function extractFromExpression(value) {
|
|
77
|
-
if (value.expression) {
|
|
78
|
-
return parser.extractVariables(value.expression);
|
|
79
|
-
}
|
|
80
|
-
return [];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (item.reverse) {
|
|
84
|
-
variables = _.uniq(_.flatten(item.reverse.map(extractFromExpression)));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
logger.debug(context, 'Extracted variables: %j', variables);
|
|
88
|
-
|
|
89
|
-
return variables;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Send a subscription for each reverse attribute defined for a device.
|
|
94
|
-
*
|
|
95
|
-
* @param {Object} device Device data object.
|
|
96
|
-
* @param {Array} attributeList List of active attributes for subscription.
|
|
97
|
-
*/
|
|
98
|
-
function sendSubscriptions(device, attributeList, callback) {
|
|
99
|
-
function sendSingleSubscriptionNgsi2(item, innerCb) {
|
|
100
|
-
const variables = extractVariables(item);
|
|
101
|
-
|
|
102
|
-
subscriptions.subscribe(device, [item.name], variables, function handleSubscription(error, subId) {
|
|
103
|
-
if (error) {
|
|
104
|
-
innerCb(error);
|
|
105
|
-
} else {
|
|
106
|
-
innerCb(null, {
|
|
107
|
-
id: subId.substr(subId.lastIndexOf('/') + 1),
|
|
108
|
-
triggers: [item.name]
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
logger.debug(context, 'Sending bidirectionality subscriptions for device [%s]', device.id);
|
|
115
|
-
|
|
116
|
-
async.map(attributeList, sendSingleSubscriptionNgsi2, callback);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Add the list of generated subscription IDs to the device object.
|
|
121
|
-
*
|
|
122
|
-
* @param {Array} subscriptionMaps List of subscription IDs to be saved.
|
|
123
|
-
* @param {Object} device Device data object.
|
|
124
|
-
* @return {Object} Modified device object.
|
|
125
|
-
*/
|
|
126
|
-
function updateDeviceWithSubscriptionIds(subscriptionMaps, device) {
|
|
127
|
-
if (!device.subscriptions) {
|
|
128
|
-
device.subscriptions = [];
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
device.subscriptions = device.subscriptions.concat(subscriptionMaps);
|
|
132
|
-
|
|
133
|
-
return device;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Middleware to handle incoming Configuration group provisions. Should check for the existence of reverse active
|
|
138
|
-
* attributes and create subscriptions for the modifciation of those values.
|
|
139
|
-
*
|
|
140
|
-
* @param {Object} newGroup Configuration Group data object.
|
|
141
|
-
*/
|
|
142
|
-
function handleGroupProvision(newGroup, callback) {
|
|
143
|
-
callback(null, newGroup);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Get a list of all the reverse transformations of a device that can be processed with the information reported by
|
|
148
|
-
* the incoming notification.
|
|
149
|
-
*
|
|
150
|
-
* @param {Array} list List of attributes to be checked
|
|
151
|
-
* @param {Array} values List of reported attributes (with their name, type and value).
|
|
152
|
-
*/
|
|
153
|
-
function getReverseTransformations(list, values, callback) {
|
|
154
|
-
const availableData = _.pluck(values, 'name');
|
|
155
|
-
let transformations = [];
|
|
156
|
-
|
|
157
|
-
function getVariable(expression) {
|
|
158
|
-
return parser.extractVariables(expression);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (list && list.length > 0) {
|
|
162
|
-
for (let i = 0; i < list.length; i++) {
|
|
163
|
-
if (list[i].reverse && list[i].reverse.length > 0) {
|
|
164
|
-
const expressions = _.pluck(list[i].reverse, 'expression');
|
|
165
|
-
const variables = _.uniq(_.flatten(expressions.map(getVariable)));
|
|
166
|
-
|
|
167
|
-
if (_.difference(variables, availableData).length === 0) {
|
|
168
|
-
transformations = transformations.concat(list[i].reverse);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
logger.debug(context, 'Got the following transformations: %j', transformations);
|
|
175
|
-
|
|
176
|
-
callback(null, transformations);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Apply a list of transformations to the reported values,
|
|
181
|
-
* generating a new array of values with the additional data.
|
|
182
|
-
* For NGSI-v2 this consists of value transformations only
|
|
183
|
-
*
|
|
184
|
-
* @param {Array} values List of reported attributes (with their name, type and value).
|
|
185
|
-
* @param {Object} typeInformation Provisioning information about the device represented by the entity.
|
|
186
|
-
* @param {Array} transformations List of transformations to apply (with their name, type and expression).
|
|
187
|
-
*/
|
|
188
|
-
function processTransformationsNGSIv2(values, typeInformation, transformations, callback) {
|
|
189
|
-
let cleanedExpression;
|
|
190
|
-
const ctx = parser.extractContext(values);
|
|
191
|
-
let resultTransformations = [];
|
|
192
|
-
|
|
193
|
-
for (let j = 0; j < 2; j++) {
|
|
194
|
-
if (transformations[j]) {
|
|
195
|
-
resultTransformations = resultTransformations.concat(transformations[j]);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
for (let i = 0; i < resultTransformations.length; i++) {
|
|
200
|
-
cleanedExpression = resultTransformations[i].expression;
|
|
201
|
-
|
|
202
|
-
values.push({
|
|
203
|
-
name: resultTransformations[i].object_id,
|
|
204
|
-
type: resultTransformations[i].type,
|
|
205
|
-
value: parser.parse(cleanedExpression, ctx, 'String', typeInformation)
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
callback(null, values);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Apply a list of transformations to the reported values,
|
|
214
|
-
* generating a new array of values with the additional data.
|
|
215
|
-
* For NGSI-LD the output includes value, metadata and datasetId
|
|
216
|
-
*
|
|
217
|
-
* @param {Array} values List of reported attributes (with their name, type and value).
|
|
218
|
-
* @param {Object} typeInformation Provisioning information about the device represented by the entity.
|
|
219
|
-
* @param {Array} transformations List of transformations to apply (with their name, type and expression).
|
|
220
|
-
*/
|
|
221
|
-
function processTransformationsNGSILD(values, typeInformation, transformations, callback) {
|
|
222
|
-
let cleanedExpression;
|
|
223
|
-
const defaultValues = [];
|
|
224
|
-
const datasetIds = {};
|
|
225
|
-
|
|
226
|
-
// Split incoming values into those with and without datasetId
|
|
227
|
-
values.forEach((value) => {
|
|
228
|
-
if (value.datasetId) {
|
|
229
|
-
datasetIds[value.datasetId] = datasetIds[value.datasetId] || [];
|
|
230
|
-
datasetIds[value.datasetId].push(value);
|
|
231
|
-
} else {
|
|
232
|
-
defaultValues.push(value);
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
let resultTransformations = [];
|
|
237
|
-
|
|
238
|
-
for (let j = 0; j < 2; j++) {
|
|
239
|
-
if (transformations[j]) {
|
|
240
|
-
resultTransformations = resultTransformations.concat(transformations[j]);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Process Transformations using the default datasetId
|
|
245
|
-
// This is the direct equivalent of the NGSI-v2 function
|
|
246
|
-
if (!_.isEmpty(defaultValues)) {
|
|
247
|
-
for (let i = 0; i < resultTransformations.length; i++) {
|
|
248
|
-
cleanedExpression = resultTransformations[i].expression;
|
|
249
|
-
values.push({
|
|
250
|
-
name: resultTransformations[i].object_id,
|
|
251
|
-
type: resultTransformations[i].type,
|
|
252
|
-
metadata: {},
|
|
253
|
-
value: parser.parse(cleanedExpression, parser.extractContext(defaultValues), 'String', typeInformation)
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Process Transformations using an explicit datasetId
|
|
259
|
-
// There is no equivalent with NGSI-v2
|
|
260
|
-
_.keys(datasetIds).forEach((datasetId) => {
|
|
261
|
-
for (let i = 0; i < resultTransformations.length; i++) {
|
|
262
|
-
cleanedExpression = resultTransformations[i].expression;
|
|
263
|
-
values.push({
|
|
264
|
-
name: resultTransformations[i].object_id,
|
|
265
|
-
type: resultTransformations[i].type,
|
|
266
|
-
datasetId,
|
|
267
|
-
metadata: {},
|
|
268
|
-
value: parser.parse(
|
|
269
|
-
cleanedExpression,
|
|
270
|
-
parser.extractContext(datasetIds[datasetId]),
|
|
271
|
-
'String',
|
|
272
|
-
typeInformation
|
|
273
|
-
)
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
callback(null, values);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Apply a list of transformations to the reported values, generating a new array of values with the additional data.
|
|
282
|
-
*
|
|
283
|
-
* @param {Array} values List of reported attributes (with their name, type and value).
|
|
284
|
-
* @param {Object} typeInformation Provisioning information about the device represented by the entity.
|
|
285
|
-
* @param {Array} transformations List of transformations to apply (with their name, type and expression).
|
|
286
|
-
*/
|
|
287
|
-
function processTransformations(values, typeInformation, transformations, callback) {
|
|
288
|
-
if (config.checkNgsiLD({})) {
|
|
289
|
-
processTransformationsNGSILD(values, typeInformation, transformations, callback);
|
|
290
|
-
} else {
|
|
291
|
-
processTransformationsNGSIv2(values, typeInformation, transformations, callback);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Middleware to handle incoming Configuration group provisions. Should check for the existence of reverse active
|
|
297
|
-
* attributes and create subscriptions for the modifciation of those values.
|
|
298
|
-
*
|
|
299
|
-
* @param {Object} device Device data object.
|
|
300
|
-
*/
|
|
301
|
-
function handleDeviceProvision(device, callback) {
|
|
302
|
-
deviceService.findConfigurationGroup(device, function (error, group) {
|
|
303
|
-
if (error) {
|
|
304
|
-
callback(error);
|
|
305
|
-
} else {
|
|
306
|
-
async.waterfall(
|
|
307
|
-
[apply(extractBidirectionalAttributes, device, group), apply(sendSubscriptions, device)],
|
|
308
|
-
function (error, subscriptionMaps) {
|
|
309
|
-
if (error) {
|
|
310
|
-
callback(error);
|
|
311
|
-
} else {
|
|
312
|
-
device = updateDeviceWithSubscriptionIds(subscriptionMaps, device);
|
|
313
|
-
callback(null, device);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Handles an incoming notification, modifying the reported values if the device has any bidirectional expression
|
|
323
|
-
* defined for its active attributes.
|
|
324
|
-
*
|
|
325
|
-
* @param {Object} device Device data object.
|
|
326
|
-
* @param {Array} values List of notified values.
|
|
327
|
-
*/
|
|
328
|
-
function handleNotification(device, values, callback) {
|
|
329
|
-
deviceService.findConfigurationGroup(device, function (error, group) {
|
|
330
|
-
const deviceAttributes = device.active || [];
|
|
331
|
-
const groupAttributes = (group && group.attributes) || [];
|
|
332
|
-
|
|
333
|
-
if (deviceAttributes.length > 0 || groupAttributes.length > 0) {
|
|
334
|
-
logger.debug(context, 'Processing active attributes notification');
|
|
335
|
-
|
|
336
|
-
async.waterfall(
|
|
337
|
-
[
|
|
338
|
-
apply(async.series, [
|
|
339
|
-
apply(getReverseTransformations, deviceAttributes, values),
|
|
340
|
-
apply(getReverseTransformations, groupAttributes, values)
|
|
341
|
-
]),
|
|
342
|
-
apply(processTransformations, values, device)
|
|
343
|
-
],
|
|
344
|
-
function (error, results) {
|
|
345
|
-
callback(error, device, results);
|
|
346
|
-
}
|
|
347
|
-
);
|
|
348
|
-
} else {
|
|
349
|
-
callback(null, device, values);
|
|
350
|
-
}
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
exports.deviceProvision = handleDeviceProvision;
|
|
355
|
-
exports.groupProvision = handleGroupProvision;
|
|
356
|
-
exports.notification = handleNotification;
|