@vida-global/core 2.0.6 → 2.0.8

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.
@@ -142,9 +142,19 @@ class BaseRecord extends Model {
142
142
  /***********************************************************************************************
143
143
  * DB CONNECTION
144
144
  ***********************************************************************************************/
145
+ static get connectionClass() {
146
+ return Connection;
147
+ }
148
+
149
+
150
+ static buildConnection() {
151
+ return new this.connectionClass(this.databaseId);
152
+ }
153
+
154
+
145
155
  static get connection() {
146
156
  if (!this._connection) {
147
- this._connection = new Connection(this.constructor.databaseId);
157
+ this._connection = this.buildConnection();
148
158
  }
149
159
  return this._connection;
150
160
  }
@@ -297,8 +307,13 @@ class BaseRecord extends Model {
297
307
  }
298
308
 
299
309
 
310
+ static get redisClientFactory() {
311
+ return redisClientFactory;
312
+ }
313
+
314
+
300
315
  static async _getRedisClient() {
301
- const client = redisClientFactory();
316
+ const client = this.redisClientFactory();
302
317
  await client.connect();
303
318
  return client;
304
319
  }
@@ -13,9 +13,34 @@ class Connection {
13
13
  #options;
14
14
 
15
15
  constructor(databaseId, options={}) {
16
- this.#databaseId = databaseId || DEFAULT_DATABASE_ID;
17
- this.#configuration = new ConnectionConfiguration(this.#databaseId);
18
- this.#options = options;
16
+ this.#databaseId = databaseId || DEFAULT_DATABASE_ID;
17
+ this.#options = options;
18
+ }
19
+
20
+
21
+ /***********************************************************************************************
22
+ * COLLABORATOR SEAMS
23
+ ***********************************************************************************************/
24
+ get ormEngineClass() {
25
+ return Sequelize;
26
+ }
27
+
28
+
29
+ get configurationClass() {
30
+ return ConnectionConfiguration;
31
+ }
32
+
33
+
34
+ get queryInterfaceClass() {
35
+ return QueryInterface;
36
+ }
37
+
38
+
39
+ get configuration() {
40
+ if (!this.#configuration) {
41
+ this.#configuration = new this.configurationClass(this.#databaseId);
42
+ }
43
+ return this.#configuration;
19
44
  }
20
45
 
21
46
 
@@ -38,14 +63,14 @@ class Connection {
38
63
 
39
64
 
40
65
  get queryInterface() {
41
- return new QueryInterface(this);
66
+ return new this.queryInterfaceClass(this);
42
67
  }
43
68
 
44
69
 
45
70
  get _sequelize() {
46
71
  if (sequelizeConnectionsCache[this.#databaseId]) return sequelizeConnectionsCache[this.#databaseId];
47
72
 
48
- const connection = new Sequelize(null, null, null, this.sequelizeOptions);
73
+ const connection = new this.ormEngineClass(null, null, null, this.sequelizeOptions);
49
74
  sequelizeConnectionsCache[this.#databaseId] = connection;
50
75
 
51
76
  return connection;
@@ -76,34 +101,34 @@ class Connection {
76
101
 
77
102
  get postgresSequelizeOptions() {
78
103
  const options = {
79
- pool: this.#configuration.pool,
104
+ pool: this.configuration.pool,
80
105
  };
81
106
 
82
107
  const primaryConfig = {
83
- database: this.#configuration.database,
84
- host: this.#configuration.host,
85
- password: this.#configuration.password,
86
- port: this.#configuration.port,
87
- username: this.#configuration.username
108
+ database: this.configuration.database,
109
+ host: this.configuration.host,
110
+ password: this.configuration.password,
111
+ port: this.configuration.port,
112
+ username: this.configuration.username
88
113
  };
89
114
 
90
- if (this.#configuration.readers.length) {
115
+ if (this.configuration.readers.length) {
91
116
  options.replication = {
92
117
  write: primaryConfig,
93
- read: this.#configuration.readers
118
+ read: this.configuration.readers
94
119
  }
95
120
  } else {
96
121
  Object.assign(options, primaryConfig);
97
122
  }
98
123
 
99
- if (this.#configuration.ssl) options.dialectOptions = {ssl: {require: true}};
124
+ if (this.configuration.ssl) options.dialectOptions = {ssl: {require: true}};
100
125
  return options;
101
126
  }
102
127
 
103
128
 
104
129
  get sqliteSequelizeOptions() {
105
130
  return {
106
- storage: this.#configuration.storage
131
+ storage: this.configuration.storage
107
132
  };
108
133
  }
109
134
 
@@ -112,12 +137,12 @@ class Connection {
112
137
  if (this.#options.hasOwnProperty('loggingEnabled')) {
113
138
  return this.#options.loggingEnabled;
114
139
  }
115
- return this.#configuration.loggingEnabled;
140
+ return this.configuration.loggingEnabled;
116
141
  }
117
142
 
118
143
 
119
144
  get dialect() {
120
- return this.#configuration.dialect;
145
+ return this.configuration.dialect;
121
146
  }
122
147
  }
123
148
 
@@ -6,35 +6,65 @@ const { logger } = require('../../logger');
6
6
  const { Sequelize } = require('sequelize');
7
7
  const utils = require('../utils');
8
8
 
9
- async function generateActiveRecordSchemaFiles() {
10
- utils.ensureSchemasDirectory();
9
+ class SchemaFileGenerator {
10
+ get connectionClass() {
11
+ return Connection;
12
+ }
13
+
11
14
 
12
- const databaseIds = ConnectionConfiguration.configuredDatabaseIds();
13
- for (const databaseId of databaseIds) {
14
- await generateSchemaFilesForDatabase(databaseId);
15
+ get databaseIds() {
16
+ return ConnectionConfiguration.configuredDatabaseIds();
15
17
  }
16
- };
17
18
 
18
19
 
19
- async function generateSchemaFilesForDatabase(databaseId) {
20
- Connection.clearConnectionsCache();
21
- const connection = new Connection(databaseId, {loggingEnabled: false});
22
- const queryInterface = connection.queryInterface;
23
- const tables = await queryInterface.allTables();
20
+ async run() {
21
+ utils.ensureSchemasDirectory();
22
+ for (const databaseId of this.databaseIds) {
23
+ await this.generateForDatabase(databaseId);
24
+ }
25
+ }
26
+
27
+
28
+ buildConnection(databaseId) {
29
+ return new this.connectionClass(databaseId, {loggingEnabled: false});
30
+ }
31
+
24
32
 
25
- const filePath = filePathForSchemaFile(databaseId);
26
- const schemas = `module.exports = ${JSON.stringify(tables, null, 4)};`;
33
+ async generateForDatabase(databaseId) {
34
+ this.connectionClass.clearConnectionsCache();
35
+ const tables = await this.fetchTables(databaseId);
36
+ this.writeSchemaFile(databaseId, tables);
37
+ }
27
38
 
28
- fs.writeFile(filePath, schemas, err => {
39
+
40
+ async fetchTables(databaseId) {
41
+ const connection = this.buildConnection(databaseId);
42
+ return await connection.queryInterface.allTables();
43
+ }
44
+
45
+
46
+ writeSchemaFile(databaseId, tables) {
47
+ const filePath = filePathForSchemaFile(databaseId);
48
+ const schemas = `module.exports = ${JSON.stringify(tables, null, 4)};`;
49
+ fs.writeFile(filePath, schemas, err => this.handleWriteResult(err, filePath));
50
+ }
51
+
52
+
53
+ handleWriteResult(err, filePath) {
29
54
  if (err) {
30
55
  logger.error(`Unable to write new schema file to ${filePath}`);
31
56
  } else {
32
- logger.info(`New schemas file written to ${filePath}`);
57
+ logger.info(`New schemas file written to ${filePath}`);
33
58
  }
34
- });
59
+ }
35
60
  }
36
61
 
37
62
 
63
+ async function generateActiveRecordSchemaFiles() {
64
+ await new SchemaFileGenerator().run();
65
+ };
66
+
67
+
38
68
  function getActiveRecordSchema(tableName, databaseId, dialect) {
39
69
  const filePath = filePathForSchemaFile(databaseId);
40
70
  const schema = importSchema(filePath, tableName);
@@ -108,6 +138,7 @@ function filePathForSchemaFile(databaseId) {
108
138
 
109
139
 
110
140
  module.exports = {
141
+ SchemaFileGenerator,
111
142
  generateActiveRecordSchemaFiles,
112
143
  getActiveRecordSchema
113
144
  };
@@ -20,8 +20,13 @@ class AbstractJob {
20
20
  }
21
21
 
22
22
 
23
+ static get queueResolver() {
24
+ return getQueue;
25
+ }
26
+
27
+
23
28
  static queue(...jobArgs) {
24
- return getQueue(this.queueName(...jobArgs));
29
+ return this.queueResolver(this.queueName(...jobArgs));
25
30
  }
26
31
 
27
32
 
@@ -43,8 +48,18 @@ class AbstractJob {
43
48
  }
44
49
 
45
50
 
51
+ get loggerLibrary() {
52
+ return logger;
53
+ }
54
+
55
+
56
+ get apm() {
57
+ return APM;
58
+ }
59
+
60
+
46
61
  get logger() {
47
- return logger[this.constructor.name].child(this.id);
62
+ return this.loggerLibrary[this.constructor.name].child(this.id);
48
63
  }
49
64
 
50
65
 
@@ -57,12 +72,12 @@ class AbstractJob {
57
72
  * APM
58
73
  ***********************************************************************************************/
59
74
  addApmAttribute(key, value) {
60
- APM.addCustomAttribute(key, value);
75
+ this.apm.addCustomAttribute(key, value);
61
76
  }
62
77
 
63
78
 
64
79
  startApmSegment(name, record, handler) {
65
- return APM.startSegment(name, record, handler);
80
+ return this.apm.startSegment(name, record, handler);
66
81
  }
67
82
 
68
83
 
@@ -18,12 +18,15 @@ class Queue extends AbstractJobComponent {
18
18
  }
19
19
 
20
20
 
21
- get queueName() { return this.#queueName }
22
- get logger() { return logger.queue.child(this.fullQueueName) }
21
+ get queueName() { return this.#queueName }
22
+ get loggerLibrary() { return logger }
23
+ get logger() { return this.loggerLibrary.queue.child(this.fullQueueName) }
24
+ get queueClass() { return BullMQ.Queue }
25
+ get idGenerator() { return randomUUID }
23
26
 
24
27
 
25
28
  async queueJob(job, args, settings) {
26
- const id = randomUUID();
29
+ const id = this.idGenerator();
27
30
  await this.#queue.add(job.name, { id, args }, settings);
28
31
  }
29
32
 
@@ -34,7 +37,7 @@ class Queue extends AbstractJobComponent {
34
37
  get #queue() {
35
38
  if (this.#_queue) return this.#_queue;
36
39
 
37
- this.#_queue = new BullMQ.Queue(this.fullQueueName, this.queueOptions);
40
+ this.#_queue = new this.queueClass(this.fullQueueName, this.queueOptions);
38
41
  this.#_queue.on('error', this.#handleError.bind(this));
39
42
 
40
43
  return this.#_queue
@@ -157,5 +160,6 @@ function getQueue(name) {
157
160
 
158
161
 
159
162
  module.exports = {
163
+ Queue,
160
164
  getQueue
161
165
  }
@@ -54,8 +54,18 @@ class Worker extends AbstractJobComponent {
54
54
  }
55
55
 
56
56
 
57
+ get loggerLibrary() {
58
+ return logger;
59
+ }
60
+
61
+
62
+ get apm() {
63
+ return APM;
64
+ }
65
+
66
+
57
67
  get logger() {
58
- return logger.worker;
68
+ return this.loggerLibrary.worker;
59
69
  }
60
70
 
61
71
 
@@ -67,12 +77,17 @@ class Worker extends AbstractJobComponent {
67
77
  /***********************************************************************************************
68
78
  * WORKER SETUP
69
79
  ***********************************************************************************************/
80
+ get workerClass() {
81
+ return BullMQ.Worker;
82
+ }
83
+
84
+
70
85
  get #worker() {
71
86
  if (this.#_worker) return this.#_worker;
72
87
 
73
- this.#_worker = new BullMQ.Worker(this.fullQueueName,
74
- this.processJob.bind(this),
75
- this.workerOptions);
88
+ this.#_worker = new this.workerClass(this.fullQueueName,
89
+ this.processJob.bind(this),
90
+ this.workerOptions);
76
91
  this.#setupEventHandling();
77
92
 
78
93
  return this.#_worker;
@@ -108,9 +123,9 @@ class Worker extends AbstractJobComponent {
108
123
  if (!jobClass) throw `Unknown Job: ${bullJob.name}`;
109
124
 
110
125
  const context = this.#buildJobContext(bullJob, jobClass);
111
- return await APM.startBackgroundTransaction(context.jobName,
112
- this.fullQueueName,
113
- () => this.#runJob(context));
126
+ return await this.apm.startBackgroundTransaction(context.jobName,
127
+ this.fullQueueName,
128
+ () => this.#runJob(context));
114
129
  }
115
130
 
116
131
 
@@ -149,25 +164,25 @@ class Worker extends AbstractJobComponent {
149
164
  async #executeJob(context) {
150
165
  const { bullJob, jobClass, id, args, jobName } = context;
151
166
  const job = new jobClass();
152
- await APM.startSegment(`job:${jobName}`,
153
- true,
154
- () => job._run(id, args, bullJob.updateProgress.bind(bullJob)));
167
+ await this.apm.startSegment(`job:${jobName}`,
168
+ true,
169
+ () => job._run(id, args, bullJob.updateProgress.bind(bullJob)));
155
170
  }
156
171
 
157
172
 
158
173
  #addJobAttributes(context) {
159
- APM.addCustomAttribute('jobId', context.id);
160
- APM.addCustomAttribute('jobName', context.jobName);
161
- APM.addCustomAttribute('queueName', context.queueName);
162
- APM.addCustomAttribute('attempt', context.attempt);
163
- APM.addCustomAttribute('maxAttempts', context.maxAttempts);
174
+ this.apm.addCustomAttribute('jobId', context.id);
175
+ this.apm.addCustomAttribute('jobName', context.jobName);
176
+ this.apm.addCustomAttribute('queueName', context.queueName);
177
+ this.apm.addCustomAttribute('attempt', context.attempt);
178
+ this.apm.addCustomAttribute('maxAttempts', context.maxAttempts);
164
179
  }
165
180
 
166
181
 
167
182
  #reportJobError(err, context) {
168
183
  const isFinalRetry = context.attempt >= context.maxAttempts;
169
184
 
170
- APM.reportError(err, {
185
+ this.apm.reportError(err, {
171
186
  jobId: context.id,
172
187
  jobName: context.jobName,
173
188
  queueName: context.queueName,
@@ -181,7 +196,7 @@ class Worker extends AbstractJobComponent {
181
196
 
182
197
 
183
198
  #recordJobFailedEvent(context, err) {
184
- APM.recordCustomEvent(JOB_FAILED_EVENT, {
199
+ this.apm.recordCustomEvent(JOB_FAILED_EVENT, {
185
200
  jobName: context.jobName,
186
201
  queueName: context.queueName,
187
202
  attempts: context.attempt,
@@ -191,13 +206,13 @@ class Worker extends AbstractJobComponent {
191
206
 
192
207
 
193
208
  #recordJobSuccess(context, duration) {
194
- APM.recordCustomEvent(JOB_COMPLETED_EVENT, {
209
+ this.apm.recordCustomEvent(JOB_COMPLETED_EVENT, {
195
210
  jobName: context.jobName,
196
211
  queueName: context.queueName,
197
212
  durationMs: duration,
198
213
  attempts: context.attempt,
199
214
  });
200
- APM.recordMetric(durationMetricName(context.queueName, context.jobName), duration);
215
+ this.apm.recordMetric(durationMetricName(context.queueName, context.jobName), duration);
201
216
  }
202
217
 
203
218
 
@@ -247,7 +262,7 @@ class Worker extends AbstractJobComponent {
247
262
 
248
263
  #loggerForJob(bullJob) {
249
264
  const { id } = this.#dataForJob(bullJob);
250
- return logger.worker.child(id);
265
+ return this.logger.child(id);
251
266
  }
252
267
 
253
268
 
@@ -92,13 +92,17 @@ class Logger {
92
92
 
93
93
 
94
94
  #createLogger(scopes, id) {
95
- scopes = Array.isArray(scopes) ? scopes : [scopes];
96
- scopes = scopes.map(scope => scope.toLowerCase());
97
- const key = `${scopes.join('|')}:${id}`;
98
- if (loggers[key]) return loggers[key];
95
+ scopes = Array.isArray(scopes) ? scopes : [scopes];
96
+ scopes = scopes.map(scope => scope.toLowerCase());
97
+ const cacheKey = scopes.join('|');
99
98
 
100
- loggers[key] = new Logger(scopes, id);
101
- return loggers[key];
99
+ if (!id && loggers[cacheKey]) return loggers[cacheKey];
100
+
101
+ const logger = new Logger(scopes, id);
102
+
103
+ if (!id) loggers[cacheKey] = logger;
104
+
105
+ return logger;
102
106
  }
103
107
 
104
108
 
@@ -1,41 +1,56 @@
1
1
  const expressWinston = require('express-winston');
2
2
 
3
3
 
4
- const msgFormatter = (req, res) => {
5
- let msg;
6
- if (req.controller) {
7
- msg = `${req.controller.constructor.name}#${req.action}`;
8
- } else {
9
- msg = res.statusCode >= 500 ? 'request errored' : 'request completed';
4
+ class LoggingMiddleware {
5
+ #logger;
6
+ #middleware;
7
+
8
+
9
+ constructor(logger) {
10
+ this.#logger = logger;
11
+ }
12
+
13
+
14
+ get loggerMiddlewareClass() {
15
+ return expressWinston;
10
16
  }
11
17
 
12
- const responseTime = res.responseTime || '-';
13
- const requestDetails = `req: ${req.method} ${req.url.split('?')[0]} for ${req.ip}`;
14
- const responseDetails = `res: statusCode=${res.statusCode} responseTime=${responseTime}ms`;
15
18
 
16
- msg = `${msg}\n ${requestDetails}\n ${responseDetails}`;
17
- if (res.error) {
18
- msg = `${msg}\n${res.error.stack}`;
19
+ get middleware() {
20
+ if (!this.#middleware) {
21
+ this.#middleware = this.loggerMiddlewareClass.logger({
22
+ dynamicMeta: (req, res) => ({ id: req.id }),
23
+ level: 'debug',
24
+ msg: this.msgFormatter.bind(this),
25
+ winstonInstance: this.#logger._winstonLogger,
26
+ });
27
+ }
28
+
29
+ return this.#middleware;
19
30
  }
20
31
 
21
- return msg;
22
- };
23
32
 
33
+ msgFormatter(req, res) {
34
+ const baseMessage = this.requestMessage(req, res);
35
+ const requestDetails = `req: ${req.method} ${req.url.split('?')[0]} for ${req.ip}`;
36
+ const responseTime = res.responseTime || '-';
37
+ const responseDetails = `res: statusCode=${res.statusCode} responseTime=${responseTime}ms`;
24
38
 
25
- let _middleware;
39
+ let msg = `${baseMessage}\n ${requestDetails}\n ${responseDetails}`;
40
+ if (res.error) msg = `${msg}\n${res.error.stack}`;
26
41
 
27
- function middleware(logger) {
28
- if (_middleware) return _middleware;
29
- _middleware = expressWinston.logger({
30
- dynamicMeta: (req, res) => ({id: req.id}),
31
- level: 'debug',
32
- msg: msgFormatter,
33
- winstonInstance: logger._winstonLogger,
34
- });
35
- return _middleware;
36
- };
42
+ return msg;
43
+ }
44
+
45
+
46
+ requestMessage(req, res) {
47
+ if (req.controller) return `${req.controller.constructor.name}#${req.action}`;
48
+ if (res.statusCode >= 500) return 'request errored';
49
+ return 'request completed';
50
+ }
51
+ }
37
52
 
38
53
 
39
54
  module.exports = {
40
- middleware
55
+ LoggingMiddleware
41
56
  }
@@ -231,11 +231,13 @@ const InstanceMethods = {
231
231
  if (!this._isStreaming) throw new NoActiveStreamError();
232
232
  if (this._streamAbortController?.signal?.aborted || this.writableEnded) return;
233
233
 
234
+ let msg;
234
235
  if (evt) {
235
- this._response.write(`event: ${evt}\n`);
236
+ msg = `event: ${evt}\ndata: ${data}\n\n`;
237
+ } else {
238
+ msg = `data: ${data}\n\n`;
236
239
  }
237
240
 
238
- const msg = `data: ${data}\n\n`;
239
241
  this._response.write(msg);
240
242
  }
241
243
  }
@@ -3,10 +3,10 @@ const cookieParser = require('cookie-parser');
3
3
  const { ControllerImporter } = require('./controllerImporter');
4
4
  const express = require('express');
5
5
  const { logger } = require('../logger');
6
- const loggingMiddleware = require('../logger/serverMiddleware');
6
+ const { LoggingMiddleware } = require('../logger/serverMiddleware');
7
7
  const mustacheExpress = require('mustache-express');
8
8
  const responseTime = require('response-time');
9
- const IoServer = require("socket.io")
9
+ const IoServer = require("socket.io");
10
10
  const { Server } = require('http');
11
11
  const { SystemController } = require('./systemController');
12
12
  const requestID = require( 'express-request-id');
@@ -50,9 +50,14 @@ class VidaServer {
50
50
  /***********************************************************************************************
51
51
  * WEB SOCKETS
52
52
  ***********************************************************************************************/
53
+ get socketServerFactory() {
54
+ return IoServer;
55
+ }
56
+
57
+
53
58
  get #ioServer() {
54
59
  if (!this.#_ioServer) {
55
- this.#_ioServer = IoServer(this.#httpServer, {
60
+ this.#_ioServer = this.socketServerFactory(this.#httpServer, {
56
61
  cors: { origin: this.corsOrigins, credentials: true }
57
62
  });
58
63
  }
@@ -79,9 +84,9 @@ class VidaServer {
79
84
  this.use(this.octetStreamParsingMiddleware);
80
85
  this.use(responseTime({suffix: true}))
81
86
  this.use(cookieParser());
82
- this.use(express.static('public'))
87
+ this.use(this.serverEngineFactory.static('public'))
83
88
  this.use(this.loggingMiddleware);
84
- this.use('/static', express.static(this.staticFilesDirectory));
89
+ this.use('/static', this.serverEngineFactory.static(this.staticFilesDirectory));
85
90
  this.use(this.connectionAbortedMiddleware);
86
91
  this.use(requestID());
87
92
 
@@ -90,7 +95,7 @@ class VidaServer {
90
95
 
91
96
 
92
97
  get jsonParsingMiddleware() {
93
- return express.json({
98
+ return this.serverEngineFactory.json({
94
99
  limit: '50Mb', // Increase JSON limit
95
100
  parameterLimit: 50000, // Increase parameter limit
96
101
  type: [
@@ -102,7 +107,7 @@ class VidaServer {
102
107
 
103
108
 
104
109
  get octetStreamParsingMiddleware() {
105
- return express.raw({ type: 'application/octet-stream', limit: this.octetStreamLimit });
110
+ return this.serverEngineFactory.raw({ type: 'application/octet-stream', limit: this.octetStreamLimit });
106
111
  }
107
112
 
108
113
 
@@ -112,7 +117,7 @@ class VidaServer {
112
117
 
113
118
 
114
119
  get loggingMiddleware() {
115
- return loggingMiddleware.middleware(this.logger);
120
+ return new LoggingMiddleware(this.logger).middleware;
116
121
  }
117
122
 
118
123
 
@@ -235,14 +240,24 @@ class VidaServer {
235
240
  }
236
241
 
237
242
 
243
+ get serverEngineFactory() {
244
+ return express;
245
+ }
246
+
247
+
248
+ get httpServerFactory() {
249
+ return Server;
250
+ }
251
+
252
+
238
253
  get #expressServer() {
239
- if (!this.#_expressServer) this.#_expressServer = express();
254
+ if (!this.#_expressServer) this.#_expressServer = this.serverEngineFactory();
240
255
  return this.#_expressServer;
241
256
  }
242
257
 
243
258
 
244
259
  get #httpServer() { // Needed for io socket to work
245
- if (!this.#_httpServer) this.#_httpServer = Server(this.#expressServer);
260
+ if (!this.#_httpServer) this.#_httpServer = this.httpServerFactory(this.#expressServer);
246
261
  return this.#_httpServer;
247
262
  }
248
263
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vida-global/core",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
4
4
  "description": "Core libraries for supporting Vida development",
5
5
  "author": "",
6
6
  "license": "ISC",