@sedni/cloud_common 1.0.0

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/.eslintrc.js ADDED
@@ -0,0 +1,32 @@
1
+ module.exports = {
2
+ env: {
3
+ node: true,
4
+ commonjs: true,
5
+ es2021: true,
6
+ mocha: true,
7
+ },
8
+ extends: [ "eslint:recommended", "plugin:node/recommended" ],
9
+ parserOptions: {
10
+ ecmaVersion: "latest",
11
+ },
12
+ rules: {
13
+ indent: [ "error", 4, { SwitchCase: 1 } ],
14
+ quotes: [ "error", "double" ],
15
+ semi: [ "error", "always" ],
16
+ "no-unused-vars": [
17
+ "error",
18
+ {
19
+ varsIgnorePattern: "should|expect|supertest|assert",
20
+ },
21
+ ],
22
+ curly: [ "error", "all" ],
23
+ "brace-style": [ "error", "allman" ],
24
+ "node/no-unpublished-require": [
25
+ "error",
26
+ {
27
+ allowModules: [ "swagger-ui-express" ],
28
+ },
29
+ ],
30
+ "node/no-missing-require": "off",
31
+ },
32
+ };
@@ -0,0 +1,70 @@
1
+ const mongoose = require("mongoose");
2
+ const mongoosePaginate = require("mongoose-paginate-v2");
3
+ const mongooseAggregatePaginate = require("mongoose-aggregate-paginate-v2");
4
+
5
+ const channelSchema = new mongoose.Schema({
6
+ channel_tag: {
7
+ type: String,
8
+ required: true,
9
+ unique: true,
10
+ index: true,
11
+ },
12
+ channel_description: {
13
+ type: String,
14
+ required: true,
15
+ index: true,
16
+ },
17
+ channel_unit_id: {
18
+ type: String,
19
+ required: true,
20
+ },
21
+ channel_parsed : {
22
+ type: Object,
23
+ required: true,
24
+ },
25
+ channel_last_bucket_sync: {
26
+ type: Date,
27
+ required: true,
28
+ },
29
+ }, {
30
+ timestamps: {
31
+ createdAt: true,
32
+ updatedAt: true
33
+ },
34
+ collection: "channels",
35
+ toJSON: {
36
+ transform: function (doc, ret)
37
+ {
38
+ ret.id = ret._id;
39
+ delete ret._id;
40
+ delete ret.__v;
41
+ },
42
+ },
43
+ });
44
+
45
+ /**
46
+ * ///////////////////////////////////////////////
47
+ * ///////////// INDEXES /////////////////
48
+ * ///////////////////////////////////////////////
49
+ */
50
+
51
+ /**
52
+ * Index used primarily for filtering by unit_id
53
+ */
54
+ channelSchema.index({ "unit_id" : 1 });
55
+
56
+ /**
57
+ * Index used primarily for filtering by unit_internal_description
58
+ */
59
+ channelSchema.index({ "unit_internal_description" : 1 });
60
+
61
+
62
+ /**
63
+ * ///////////////////////////////////////////////
64
+ * ///////////// PLUGINS /////////////////
65
+ * ///////////////////////////////////////////////
66
+ */
67
+ channelSchema.plugin(mongoosePaginate);
68
+ channelSchema.plugin(mongooseAggregatePaginate);
69
+
70
+ module.exports = channelSchema;
@@ -0,0 +1,255 @@
1
+ const mongoose = require("mongoose");
2
+ const mongoose_paginate = require("mongoose-paginate-v2");
3
+ const mongoose_aggregate_paginate = require("mongoose-aggregate-paginate-v2");
4
+
5
+ const channeldataBucketSchema = new mongoose.Schema({
6
+ start_date: {
7
+ type: Date,
8
+ required: true,
9
+ default: Date.now,
10
+ },
11
+ end_date: {
12
+ type: Date,
13
+ required: true,
14
+ default: function()
15
+ {
16
+ return new Date(Date.now() + 1000 * 60 * 60); // 1 hour from now
17
+ }
18
+ },
19
+ data : [{
20
+ timestamp: {
21
+ type: Number,
22
+ required: true,
23
+ },
24
+ value: {
25
+ type: Number,
26
+ required: true,
27
+ }
28
+ }],
29
+ size : {
30
+ type: Number,
31
+ required: true,
32
+ default: 0,
33
+ },
34
+ synced : {
35
+ type: Number,
36
+ required: true,
37
+ default: 0,
38
+ },
39
+ sum : {
40
+ type: Number,
41
+ required: true,
42
+ default: 0,
43
+ },
44
+ }, {
45
+ toJSON: {
46
+ transform: function (doc, ret)
47
+ {
48
+ ret.id = ret._id;
49
+ delete ret._id;
50
+ delete ret.__v;
51
+ ret.start_date = ret.start_date.getTime();
52
+ ret.end_date = ret.end_date.getTime();
53
+ ret.avg = ret.sum / ret.size;
54
+ },
55
+ },
56
+ versionKey: false,
57
+ });
58
+
59
+ /**
60
+ * ///////////////////////////////////////////////
61
+ * ///////////// INDEXES /////////////////
62
+ * ///////////////////////////////////////////////
63
+ */
64
+
65
+ /**
66
+ * Index the start_date and end_date fields for faster queries.
67
+ * The compound index will be used to query the buckets by date range.
68
+ */
69
+ channeldataBucketSchema.index({ start_date: 1, end_date: 1 });
70
+
71
+ /**
72
+ * ///////////////////////////////////////////////
73
+ * ///////////// PLUGINS /////////////////
74
+ * ///////////////////////////////////////////////
75
+ */
76
+ channeldataBucketSchema.plugin(mongoose_paginate);
77
+ channeldataBucketSchema.plugin(mongoose_aggregate_paginate);
78
+
79
+
80
+ /**
81
+ * ///////////////////////////////////////////////
82
+ * //////////// FUNCTIONS ////////////////
83
+ * ///////////////////////////////////////////////
84
+ */
85
+ /**
86
+ * Insert a data point into a specific bucket.
87
+ *
88
+ * The data point will be inserted into the bucket and the size and sum will be updated.
89
+ * @param {Number} timestamp The timestamp of the data point
90
+ * @param {Number} value The value of the data point
91
+ */
92
+ channeldataBucketSchema.methods.insertDataPoint = async function(timestamp, value)
93
+ {
94
+ this.data.push({ timestamp: timestamp, value: value });
95
+ this.size++;
96
+ this.sum += value;
97
+ await this.save();
98
+ };
99
+
100
+ /**
101
+ * Check if the bucket is closed, meaning that it is no longer accepting data points.
102
+ * @param {Number} timestamp The timestamp to check if the bucket is closed
103
+ * @returns {Boolean} True if the bucket is closed, false otherwise
104
+ */
105
+ channeldataBucketSchema.methods.isBucketClosed = function(timestamp = Date.now())
106
+ {
107
+ return timestamp >= this.end_date.getTime();
108
+ };
109
+
110
+ /**
111
+ * Insert a data point into the corresponding bucket.
112
+ *
113
+ * If no bucket exists for the timestamp, it will be created.
114
+ * If a bucket exists for the timestamp, the data point will be inserted into the bucket and the size and sum will be updated.
115
+ * @param {Number} timestamp The timestamp of the data point
116
+ * @param {Number} value The value of the data point
117
+ * @returns {Object} The bucket that the data point was inserted into
118
+ */
119
+ channeldataBucketSchema.statics.insertDataPoint = async function(timestamp, value)
120
+ {
121
+ // Truncate the timestamp to the nearest hour
122
+ const date = new Date(timestamp);
123
+ date.setMinutes(0);
124
+ date.setSeconds(0);
125
+ date.setMilliseconds(0);
126
+
127
+ // Upsert the data point
128
+ const bucket = await this.updateOne(
129
+ {
130
+ start_date: date
131
+ },
132
+ {
133
+ $push:
134
+ {
135
+ data:
136
+ {
137
+ timestamp: timestamp,
138
+ value: value
139
+ }
140
+ },
141
+
142
+ $inc:
143
+ {
144
+ size: 1,
145
+ sum: value
146
+ },
147
+
148
+ $set:
149
+ {
150
+ end_date: new Date(date.getTime() + 1000 * 60 * 60),
151
+ }
152
+ },
153
+ {
154
+ upsert: true
155
+ }
156
+ );
157
+
158
+ return bucket;
159
+ };
160
+
161
+ /**
162
+ * Get the last time the data was synced
163
+ * @param {Object} options The options to get the last time the data was synced
164
+ * @param {String} options.channel_tag The channel tag to get the data from
165
+ * @param {Object} options.db The database to get the channel document from
166
+ * @returns {Number} The last time the data was synced
167
+ */
168
+ channeldataBucketSchema.statics.lastTimeSynced = async function(options)
169
+ {
170
+ let last_sync = 0;
171
+ if(options?.channel_tag && options?.db)
172
+ {
173
+ // Get the last time the data was synced
174
+ const channel = await options.db.collection("channels").findOne({
175
+ channel_tag : options.channel_tag
176
+ });
177
+
178
+ last_sync = channel.channel_last_bucket_sync.getTime();
179
+ }
180
+ return last_sync;
181
+ };
182
+
183
+ /**
184
+ * Get the data that has not been uploaded to the cloud yet
185
+ * @param {Object} options The options to get the data
186
+ * @param {Date | Number} options.last_sync The last time the data was synced, if not provided it will be fetched from the channel document
187
+ * @param {String} options.channel_tag The channel tag to get the data from, only used if last_sync is not provided
188
+ * @param {Object} options.db The database to get the channel document from, only used if last_sync is not provided
189
+ * @returns {Array} The data that has not been uploaded to the cloud yet
190
+ */
191
+ channeldataBucketSchema.statics.getNotUploadedData = async function(options)
192
+ {
193
+ // Get the last time the data was synced
194
+ let last_sync = options?.last_sync ?? await this.lastTimeSynced(options);
195
+
196
+ // Get all the buckets that synced is lt size and end_date is gt last_sync
197
+ const buckets = await this.find({
198
+ end_date: {
199
+ $gt: new Date(last_sync)
200
+ }
201
+ });
202
+
203
+ return buckets;
204
+ };
205
+
206
+ /**
207
+ * Get the data in the specified date range
208
+ * @param {Date | Number} start_date The start date of the date range
209
+ * @param {Date | Number} end_date The end date of the date range
210
+ * @param {Boolean} plain If true, the data will be returned as an array of data points, otherwise it will be returned as an array of buckets
211
+ * @returns {Array} The data in the specified date range. It will be an array of data points if plain is true, otherwise it will be an array of buckets
212
+ */
213
+ channeldataBucketSchema.statics.getData = async function(start_date, end_date, plain = false)
214
+ {
215
+ // Get the data in the specified date range
216
+ const data = await this.find({
217
+ start_date: {
218
+ $gte: new Date(start_date),
219
+ $lt: new Date(end_date)
220
+ }
221
+ });
222
+
223
+ return plain ? data.flatMap(bucket => bucket.data) : data;
224
+ };
225
+
226
+
227
+ channeldataBucketSchema.statics.upsertBucket = async function(bucket)
228
+ {
229
+ const date = new Date(bucket.start_date);
230
+ date.setMinutes(0);
231
+ date.setSeconds(0);
232
+ date.setMilliseconds(0);
233
+
234
+ const result = await this.updateOne(
235
+ {
236
+ start_date: date
237
+ },
238
+ {
239
+ $set:
240
+ {
241
+ data: bucket.data,
242
+ size: bucket.size,
243
+ sum: bucket.sum,
244
+ end_date: new Date(date.getTime() + 1000 * 60 * 60),
245
+ }
246
+ },
247
+ {
248
+ upsert: true
249
+ }
250
+ );
251
+
252
+ return result;
253
+ };
254
+
255
+ module.exports = channeldataBucketSchema;
@@ -0,0 +1,78 @@
1
+ const mongoose = require("mongoose");
2
+ const mongoose_paginate = require("mongoose-paginate-v2");
3
+ const mongoose_aggregate_paginate = require("mongoose-aggregate-paginate-v2");
4
+ const { EventCategories } = require("app/types/event.types");
5
+
6
+ const eventSchema = new mongoose.Schema({
7
+ event_message: {
8
+ type: String,
9
+ required: true
10
+ },
11
+ event_source: {
12
+ type: String,
13
+ required: true
14
+ },
15
+ event_category: {
16
+ type: String,
17
+ required: true,
18
+ enum: Object.values(EventCategories)
19
+ },
20
+ event_type: {
21
+ type: String,
22
+ required: true
23
+ },
24
+ event_timestamp: {
25
+ type: Date,
26
+ default: Date.now,
27
+ expires: 60 * 60 * 24 * 7 * 365 // 1 year
28
+ },
29
+ event_data: {
30
+ type: Object
31
+ }
32
+ }, {
33
+ timestamps: {
34
+ createdAt: true,
35
+ updatedAt: false
36
+ },
37
+ versionKey: false,
38
+ capped: {
39
+ size: 1000000,
40
+ max: 10000
41
+ },
42
+ toJSON: {
43
+ transform: function (doc, ret)
44
+ {
45
+ ret.id = ret._id;
46
+ delete ret._id;
47
+ delete ret.createdAt;
48
+ ret.event_timestamp = ret.event_timestamp.getTime();
49
+ }
50
+ }
51
+ });
52
+
53
+ /**
54
+ * ///////////////////////////////////////////////
55
+ * ///////////// INDEXES /////////////////
56
+ * ///////////////////////////////////////////////
57
+ */
58
+
59
+ /**
60
+ * Index used primarily for filtering by channel_tag
61
+ */
62
+ historySchema.index({ "channel_tag" : 1 });
63
+
64
+ /**
65
+ * Index used primarily for filtering by timestamp
66
+ */
67
+ historySchema.index({ "timestamp" : 1 });
68
+
69
+
70
+ /**
71
+ * ///////////////////////////////////////////////
72
+ * ///////////// PLUGINS /////////////////
73
+ * ///////////////////////////////////////////////
74
+ */
75
+ historySchema.plugin(mongoose_paginate);
76
+ historySchema.plugin(mongoose_aggregate_paginate);
77
+
78
+ module.exports = eventSchema;
@@ -0,0 +1,101 @@
1
+ const mongoose = require("mongoose");
2
+ const mongoose_paginate = require("mongoose-paginate-v2");
3
+ const mongoose_aggregate_paginate = require("mongoose-aggregate-paginate-v2");
4
+ const { AlarmTypes, AlarmPriorities, AlarmStates, DiamarAlarmStates } = require("@types/alarm.types");
5
+
6
+ const historySchema = new mongoose.Schema({
7
+ timestamp: {
8
+ type: Date,
9
+ required: true,
10
+ default: Date.now,
11
+ },
12
+ channel_tag: {
13
+ type: String,
14
+ required: true,
15
+ },
16
+ alarm_priority: {
17
+ type: String,
18
+ required: true,
19
+ enum: Object.values(AlarmPriorities),
20
+ },
21
+ original_alarm_state: {
22
+ type: String,
23
+ required: true,
24
+ enum: Object.values(DiamarAlarmStates),
25
+ },
26
+ alarm_state: {
27
+ type: String,
28
+ required: true,
29
+ enum: Object.values(AlarmStates),
30
+ },
31
+ alarm_type: {
32
+ type: String,
33
+ required: true,
34
+ enum: Object.values(AlarmTypes),
35
+ },
36
+ alarm_value: {
37
+ type: Number,
38
+ required: false,
39
+ },
40
+ alarm_message: {
41
+ type: String,
42
+ required: false,
43
+ },
44
+ metadata: {
45
+ type: Object,
46
+ }
47
+ }, {
48
+ timestamps: {
49
+ createdAt: false,
50
+ updatedAt: false
51
+ },
52
+ versionKey: false,
53
+ collection: "history",
54
+ toJSON: {
55
+ transform: function (doc, ret)
56
+ {
57
+ ret.id = ret._id;
58
+ delete ret._id;
59
+ delete ret.__v;
60
+ ret.alarm = {
61
+ priority: ret.alarm_priority,
62
+ state: ret.original_alarm_state ?? "Inactive",
63
+ type: ret.alarm_type,
64
+ value: ret.alarm_value === null ? "inf" : `${ret.alarm_value}`,
65
+ timestamp: new Date(ret.timestamp).getTime()
66
+ };
67
+ delete ret.alarm_priority;
68
+ delete ret.alarm_state;
69
+ delete ret.alarm_type;
70
+ delete ret.alarm_value;
71
+ },
72
+ },
73
+ });
74
+
75
+ /**
76
+ * ///////////////////////////////////////////////
77
+ * ///////////// INDEXES /////////////////
78
+ * ///////////////////////////////////////////////
79
+ */
80
+
81
+ /**
82
+ * Index used primarily for filtering by channel_tag
83
+ */
84
+ historySchema.index({ "channel_tag" : 1 });
85
+
86
+ /**
87
+ * Index used primarily for filtering by timestamp
88
+ */
89
+ historySchema.index({ "timestamp" : 1 });
90
+
91
+
92
+ /**
93
+ * ///////////////////////////////////////////////
94
+ * ///////////// PLUGINS /////////////////
95
+ * ///////////////////////////////////////////////
96
+ */
97
+ historySchema.plugin(mongoose_paginate);
98
+ historySchema.plugin(mongoose_aggregate_paginate);
99
+
100
+
101
+ module.exports = historySchema;
@@ -0,0 +1,65 @@
1
+ const mongoose = require("mongoose");
2
+ const mongoosePaginate = require("mongoose-paginate-v2");
3
+ const mongooseAggregatePaginate = require("mongoose-aggregate-paginate-v2");
4
+
5
+ const unitSchema = new mongoose.Schema({
6
+ unit_id: {
7
+ type: String,
8
+ required: true,
9
+ unique: true,
10
+ },
11
+ unit_enabled: {
12
+ type: Boolean,
13
+ required: true,
14
+ },
15
+ unit_type: {
16
+ type: String,
17
+ required: true,
18
+ },
19
+ unit_internal_description: {
20
+ type: String,
21
+ required: true,
22
+ },
23
+ unit_cabinet_id: {
24
+ type: String,
25
+ required: true,
26
+ },
27
+ }, {
28
+ timestamps: true,
29
+ collection: "units",
30
+ toJSON: {
31
+ transform: function (doc, ret)
32
+ {
33
+ ret.id = ret._id;
34
+ delete ret._id;
35
+ delete ret.__v;
36
+ }
37
+ }
38
+ });
39
+
40
+ /**
41
+ * ///////////////////////////////////////////////
42
+ * ///////////// INDEXES /////////////////
43
+ * ///////////////////////////////////////////////
44
+ */
45
+
46
+ /**
47
+ * Index used primarily for filtering by unit_id
48
+ */
49
+ unitSchema.index({ "unit_id" : 1 });
50
+
51
+ /**
52
+ * Index used primarily for filtering by unit_internal_description
53
+ */
54
+ unitSchema.index({ "unit_internal_description" : 1 });
55
+
56
+
57
+ /**
58
+ * ///////////////////////////////////////////////
59
+ * ///////////// PLUGINS /////////////////
60
+ * ///////////////////////////////////////////////
61
+ */
62
+ unitSchema.plugin(mongoosePaginate);
63
+ unitSchema.plugin(mongooseAggregatePaginate);
64
+
65
+ module.exports = unitSchema;
@@ -0,0 +1,3 @@
1
+ {
2
+
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+
3
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+
3
+ }
@@ -0,0 +1,51 @@
1
+ const AlarmPriorities =
2
+ {
3
+ CRITICAL: "Critical",
4
+ ALARM: "Alarm",
5
+ WARNING: "Warning",
6
+ }
7
+
8
+ const DiamarAlarmStates =
9
+ {
10
+ INACTIVE: "Inactive",
11
+ ACKNOWLEDGED: "Acknowledged",
12
+ ACTIVE: "Active",
13
+ UNACKNOWLEDGED: "Unacknowledged",
14
+ UNDEFINED: "Undefined",
15
+ };
16
+
17
+ const CloudAlarmStates =
18
+ {
19
+ INACTIVE: "Inactive",
20
+ ALARM: "Active",
21
+ ALARM_ACK: "Acknowledged",
22
+ WARNING: "WarningActive",
23
+ WARNING_ACK: "WarningAcknowledged",
24
+ RETURN_NO_ACK: "Unacknowledged",
25
+ INHIBITED: "Inhibited",
26
+ };
27
+
28
+ const AlarmTypes =
29
+ {
30
+ NORMAL: "Normal",
31
+ ALARM_OPEN: "AlarmOpen",
32
+ ALARM_CLOSE: "AlarmClose",
33
+ ALARM_IFH: "AlarmIfh",
34
+ ALARM_HH: "AlarmHh",
35
+ ALARM_H: "AlarmH",
36
+ ALARM_L: "AlarmL",
37
+ ALARM_LL: "AlarmLl",
38
+ ALARM_IFL: "AlarmIfl",
39
+ ALARM_OFFSCAN: "AlarmOffscan",
40
+ ALARM_FAIL: "AlarmFail",
41
+ ALARM_INH: "AlarmInh",
42
+ ALARM_UNK: "AlarmUnk",
43
+ }
44
+
45
+ module.exports =
46
+ {
47
+ AlarmPriorities,
48
+ DiamarAlarmStates,
49
+ CloudAlarmStates,
50
+ AlarmTypes,
51
+ };
@@ -0,0 +1,14 @@
1
+ const EventCategories =
2
+ {
3
+ ACCESS_CONTROL: "ACCESS_CONTROL",
4
+ REQUEST_FAILURE: "REQUEST_FAILURE",
5
+ OS: "OS",
6
+ CONTROL: "CONTROL",
7
+ BACKUP: "BACKUP",
8
+ CONFIG_CHANGE: "CONFIG_CHANGE",
9
+ LOGS: "LOGS"
10
+ };
11
+
12
+ module.exports = {
13
+ EventCategories,
14
+ };
package/index.js ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * SCHEMAS
3
+ */
4
+ const Channel = require("app/models/channel");
5
+ const ChannelDataBucket = require("app/models/channelDataBucket");
6
+ const Event = require("app/models/event");
7
+ const History = require("app/models/history");
8
+ const Unit = require("app/models/unit");
9
+
10
+ const Schemas = {
11
+ Channel,
12
+ ChannelDataBucket,
13
+ Event,
14
+ History,
15
+ Unit,
16
+ };
17
+
18
+ /**
19
+ * DOCS
20
+ */
21
+ const ChannelDocs = require("app/models/docs/channel.docs");
22
+ const ChannelDataBucketDocs = require("app/models/docs/channelDataBucket.docs");
23
+ const EventDocs = require("app/models/docs/event.docs");
24
+ const HistoryDocs = require("app/models/docs/history.docs");
25
+ const UnitDocs = require("app/models/docs/unit.docs");
26
+
27
+ const Docs = {
28
+ ChannelDocs,
29
+ ChannelDataBucketDocs,
30
+ EventDocs,
31
+ HistoryDocs,
32
+ UnitDocs,
33
+ };
34
+
35
+ /**
36
+ * TYPES
37
+ */
38
+ const { AlarmTypes, AlarmPriorities, AlarmStates, DiamarAlarmStates } = require("app/types/alarm.types");
39
+ const { EventCategories } = require("app/types/event.types");
40
+
41
+ const Types = {
42
+ AlarmTypes,
43
+ AlarmPriorities,
44
+ AlarmStates,
45
+ DiamarAlarmStates,
46
+ EventCategories,
47
+ };
48
+
49
+ module.exports = {
50
+ Schemas,
51
+ Docs,
52
+ Types,
53
+ };
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@sedni/cloud_common",
3
+ "version": "1.0.0",
4
+ "description": "Common package for all types, resources and tools of Diamar Cloud",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "mocha",
8
+ "coverage": "nyc --include 'app/**/*controller.js' mocha",
9
+ "lint": "eslint app/*",
10
+ "lint:fix": "eslint --fix app/*"
11
+ },
12
+ "author": "Jose Luis Silvestre García",
13
+ "license": "ISC",
14
+ "dependencies": {
15
+ "mongoose": "^8.7.1",
16
+ "mongoose-aggregate-paginate-v2": "^1.1.2",
17
+ "mongoose-paginate-v2": "^1.8.5"
18
+ },
19
+ "devDependencies": {
20
+ "chai": "^5.1.1",
21
+ "eslint": "^8.57.0",
22
+ "eslint-plugin-node": "^11.1.0",
23
+ "mocha": "^10.7.3",
24
+ "nyc": "^17.0.0",
25
+ "supertest": "^7.0.0"
26
+ }
27
+ }