feat(db): add mongodb connection

additionally implements command to add greetings for test
This commit is contained in:
Jon Deaves
2020-08-13 23:17:57 +01:00
committed by GitHub
parent 55d8c27d25
commit 62170014d2
13 changed files with 432 additions and 15 deletions

View File

@@ -17,6 +17,8 @@ At a minimum you need to provide the Discord bots Token, which can be found on t
| DISCORD_BOT_TOKEN | Discord bots Token
| NODE_ENV | What environment the bot is running in | `production`, `development` or `test` |
| LOG_LEVEL | What level of logs should be displayed in console | `error`, `warn`, `info`, `verbose`, `debug` or `silly` |
| MONGODB_URI | Full connection string for MongoDB database, include db_name if user is scoped to single database | mongodb://user:password@localhost:27017/venom_db |
| MONGODB_DB_NAME | The name of the database to use for this project | venom_db |
### Bot commands

View File

@@ -19,11 +19,13 @@
"discord.js": "~12.2.0",
"dotenv": "~8.2.0",
"inversify": "~5.0.1",
"mongodb": "~3.6.0",
"reflect-metadata": "~0.1.13",
"winston": "~3.3.3"
},
"devDependencies": {
"@types/dotenv": "~8.2.0",
"@types/mongodb": "~3.5.25",
"@types/node": "~14.0.27",
"@types/winston": "~2.4.4",
"nodemon": "~2.0.4",
@@ -35,4 +37,4 @@
"tslint-react": "~5.0.0",
"typescript": "~3.9.7"
}
}
}

View File

@@ -1,4 +1,6 @@
BOT_TRIGGER=!
DISCORD_BOT_TOKEN=[replace with own token]
MONGODB_URI=[mongo_connection_string]
MONGODB_DB_NAME=[mongo_db_name]
NODE_ENV=production
LOG_LEVEL=error

View File

@@ -5,6 +5,7 @@ import container from './inversity.config';
import ConfigService from './core/services/config.service';
import LoggerService from './core/services/logger.service';
import MongoService from './core/services/mongo.service';
import ICommand from './bot/commands/ICommand';
import rawCommands from './bot/commands';
@@ -12,10 +13,18 @@ import rawCommands from './bot/commands';
export default class App {
private _configService: ConfigService = container.resolve<ConfigService>(ConfigService);
private _loggerService: LoggerService = container.resolve<LoggerService>(LoggerService);
private _dbService: MongoService = container.resolve<MongoService>(MongoService);
private _discordClient: Discord.Client;
public async init(): Promise<void> {
try {
await this._dbService.connect();
} catch (err) {
this._loggerService.log('error', 'Cannot connect to database, exiting.');
exit(1);
}
this._discordClient = new Discord.Client();
const commandList = new Discord.Collection<string, ICommand>();
@@ -44,7 +53,7 @@ export default class App {
};
try {
await command.execute(message, args, prefix, commandList);
await command.execute(message, args, prefix, commandList, this._dbService);
} catch (error) {
this._loggerService.log('error', error.message);
message.reply('there was an error trying to follow that command!');
@@ -57,11 +66,11 @@ export default class App {
const greeting = greetings[Math.floor(Math.random() * greetings.length - 1)];
// favor
const flavors = [
"As PROMISED, grab a free pie! Courtesy of {random}!",
"The water is pure here! You should ask {random} for their water purified water for a sip!",
"Home of the sane, the smart and {random}!"
"As PROMISED, grab a free pie! Courtesy of {random}!",
"The water is pure here! You should ask {random} for their water purified water for a sip!",
"Home of the sane, the smart and {random}!"
];
const randomMember = member.guild.members.cache.random();
const randomMember = member.guild.members.cache.random();
const flavor = flavors[Math.floor(Math.random() * flavors.length - 1)];
// result
member.guild.systemChannel.send(greeting.replace('{name}', member.displayName) + " " + flavor.replace('{random}', randomMember.displayName));
@@ -73,4 +82,8 @@ export default class App {
exit(1);
});
}
public exit() {
this._dbService.disconnect();
}
}

View File

@@ -1,9 +1,10 @@
import Discord, { Collection } from 'discord.js';
import MongoService from 'src/core/services/mongo.service';
export default interface ICommand {
name: string;
aliases?: string[];
description: string;
example?: string;
execute: (message: Discord.Message, args: string[], prefix?: string, commands?: Collection<string, ICommand>) => Promise<void>,
execute: (message: Discord.Message, args: string[], prefix?: string, commands?: Collection<string, ICommand>, dbService?: MongoService) => Promise<any>,
}

View File

@@ -0,0 +1,49 @@
import Discord, { Collection } from 'discord.js';
import MongoService from '../../core/services/mongo.service';
import ConfigService from '../../core/services/config.service'
import container from '../../inversity.config';
import ICommand from './ICommand';
const prefix = container.resolve<ConfigService>(ConfigService).get('BOT_TRIGGER');
const command: ICommand = {
name: 'addgreeting',
aliases: ['ag'],
description: 'Adds a string to the list greetings used when new users connect to server! Include `{name}` in your message to replace with the new users name.',
example: `\`${prefix}addgreeting Welcome to the club {name}\``,
async execute(message: Discord.Message, args: string[], prefix?: string, commands?: Collection<string, ICommand>, dbService?: MongoService) {
// Only certain users can use this command
// TODO: Better handling of permissions for commands in a generic way
const permittedRoles = ['staff']
const isPermitted = message.member.roles.cache.some(r => permittedRoles.indexOf(r.name) !== -1);
if (!isPermitted) {
return message.author.send('Sorry but I can\'t let you add greetings!');
}
// Can't do much without a message
if (args.length === 0) {
return message.author.send('When adding a greeting you need to also provide a message!');
}
// Check for dupes
const greetingStr = args.join(' ');
const matchedMessages = await dbService.find(message.author.id, 'greetings', { message: greetingStr });
if (matchedMessages.length > 0) {
return message.author.send('That greeting has already been added!');
}
const result = await dbService.insert(message.author.id, 'greetings', [{ message: greetingStr }]);
if (!result) {
message.author.send('Uh-oh! Couldn\'t add that greeting!');
}
message.author.send('I\'ve added the greeting you told me about!');
},
};
export default command;

View File

@@ -2,10 +2,12 @@ import help from './help';
import ping from './ping';
import see from './see';
import magicball from './8ball';
import addgreeting from './addgreeting'
export default [
help,
ping,
see,
magicball
magicball,
addgreeting
]

View File

@@ -33,6 +33,8 @@ export default class ConfigService {
this.config = {
BOT_TRIGGER: process.env.BOT_TRIGGER || '!',
DISCORD_BOT_TOKEN: process.env.DISCORD_BOT_TOKEN,
MONGODB_URI: process.env.MONGODB_URI || '',
MONGODB_DB_NAME: process.env.MONGODB_DB_NAME || '',
ENVIRONMENT: (process.env.NODE_ENV as Environment) || 'development',
LOG_LEVEL: (process.env.LOG_LEVEL as LogLevel) || 'info',
};

View File

@@ -0,0 +1,243 @@
import mongodb from 'mongodb';
import { injectable } from "inversify";
import assert from 'assert';
import container from '../../inversity.config';
import ConfigService from './config.service';
import LoggerService from './logger.service';
@injectable()
export default class MongoService {
private _configService: ConfigService = container.resolve<ConfigService>(ConfigService);
private _loggerService: LoggerService = container.resolve<LoggerService>(LoggerService);
private _mongoClient: mongodb.MongoClient;
public _db: mongodb.Db;
public async connect(): Promise<void> {
return new Promise((resolve, reject) => {
this._mongoClient = new mongodb.MongoClient(this._configService.get('MONGODB_URI'), { useUnifiedTopology: true });
this._mongoClient.connect((err) => {
if (err) {
this._loggerService.log('error', 'Venom could not connect to MongoDB', err);
reject();
} else {
this._loggerService.log('info', 'Venom is connected to MongoDB');
this._db = this._mongoClient.db(this._configService.get('MONGODB_DB_NAME'));
resolve();
}
});
});
}
public disconnect() {
if (this._mongoClient && this._mongoClient.isConnected) {
this._loggerService.log('info', 'Closing connection to MongoDB');
this._mongoClient.close();
}
}
/**
* Fetches the first document that matches the query
*
* @param collection String identifier for collection document belongs to
* @param query key/value pairs of search conditions
*
* @returns Returns matched document
*
* @example findOne('123456', 'collectionName', { ident: 'generated-slug' })
*/
public async findOne(userId: string, collection: string, query: any) {
try {
this.verifyConnection();
const resp = await this._db.collection(collection).findOne(query);
this._loggerService.log('verbose', 'Fetched single document from MongoDB', {
userId,
collection,
query,
resp,
});
return resp;
} catch (err) {
this._loggerService.log('error', 'Could not find document', {
userId,
error: err,
collection,
query,
});
return false;
}
}
/**
* Fetches all documents that match the query
*
* @param collection String identifier for collection documents belong to
* @param query key/value pairs of search conditions
*
* @returns Returns matched document
*
* @example find('123456', 'collectionName', { key: 'value' })
*/
public async find(userId: string, collection: string, query: any) {
try {
this.verifyConnection();
const resp = await this._db.collection(collection).find(query).toArray();
this._loggerService.log('verbose', 'Fetched documents matching query from MongoDB', {
userId,
collection,
query,
resp,
});
return resp;
} catch (err) {
this._loggerService.log('error', 'Could not find documents', {
userId,
error: err,
collection,
query,
});
return [];
}
}
/**
* Adds one or more documents to given collection
*
* @param collection String identifier for collection document is to be stored again
* @param payload Array of objects containing the key/value pairs to be stored
*
* @returns Returns true for successful insert
*
* @example `insert('123456', 'collectionName', [{ body: 'This is a document!' }])`
*/
public async insert(userId: string, collection: string, payload: any[]) {
try {
this.verifyConnection();
const resp = await this._db.collection(collection).insert(payload);
if (resp.result.ok !== 1 || resp.insertedCount !== payload.length) {
throw new Error(JSON.stringify(resp));
}
this._loggerService.log('verbose', `Inserted ${resp.insertedCount} documents into ${collection}`, {
userId,
collection,
payload,
resp
});
return true;
} catch (err) {
this._loggerService.log('error', 'Could not insert documents to database', {
userId,
error: err,
collection,
payload
});
return false;
}
}
/**
* Update single document from MongoDB
*
* @param collection String identifier for collection documents belong to
* @param query key/value pairs of search conditions
* @param payload Objects containing the key/value pairs to be stored against existing documents
*
* @returns Returns true for successful update
*
* @example `updateMany('123456', 'collectionName', { ident: 'generated-slug' }, { secondKey: 'new data'})`
*/
public async updateMany(userId: string, collection: string, query: any, payload: any) {
try {
this.verifyConnection();
const resp = await this._db.collection(collection).updateMany(query, { $set: payload });
if (resp.result.ok !== 1) {
throw new Error(JSON.stringify(resp));
}
this._loggerService.log('verbose', `Updated documents into ${collection} with id ${resp.upsertedId}`, {
userId,
collection,
query,
payload,
resp
});
return true;
} catch (err) {
this._loggerService.log('error', 'Could not update documents', {
userId,
collection,
query,
payload,
err
});
}
}
/**
* Remove documents from MongoDB matching query
*
* @param collection String identifier for collection documents belong to
* @param query key/value pairs of search conditions
*
* @returns Returns true for successful delete
*
* @example `deleteMany('123456', 'collectionName', { ident: 'generated-slug' })`
*/
public async deleteMany(userId: string, collection: string, query: any) {
try {
this.verifyConnection();
const resp = await this._db.collection(collection).deleteMany(query);
if (resp.result.ok !== 1) {
throw new Error(JSON.stringify(resp));
}
this._loggerService.log('verbose', `Deleted documents from ${collection}`, {
userId,
collection,
query,
resp
});
return true;
} catch (err) {
this._loggerService.log('error', 'Could not delete documents', {
userId,
collection,
query,
err
});
return false
}
}
private verifyConnection() {
if (!this._mongoClient.isConnected) {
this._loggerService.log('error', 'No database connection available');
throw new Error('No database connection available');
}
}
}

View File

@@ -4,6 +4,8 @@ import Environment from "./Environment";
export default interface Config {
BOT_TRIGGER: string;
DISCORD_BOT_TOKEN: string;
MONGODB_URI: string;
MONGODB_DB_NAME: string;
ENVIRONMENT: Environment;
LOG_LEVEL: LogLevel;
}

View File

@@ -2,9 +2,11 @@ import { Container } from "inversify";
import ConfigService from "./core/services/config.service";
import LoggerService from "./core/services/logger.service";
import MongoService from "./core/services/mongo.service";
const container = new Container();
container.bind<ConfigService>(ConfigService).toSelf();
container.bind<LoggerService>(LoggerService).toSelf();
container.bind<MongoService>(MongoService).toSelf();
export default container;

View File

@@ -7,6 +7,25 @@ const app = new App();
try {
app.init();
function exitHandler() {
// Cleans up the application
app.exit();
exit();
}
// do something when app is closing
process.on('exit', exitHandler);
//catches ctrl+c event
process.on('SIGINT', exitHandler);
// catches "kill pid" (for example: nodemon restart)
process.on('SIGUSR1', exitHandler);
process.on('SIGUSR2', exitHandler);
//catches uncaught exceptions
process.on('uncaughtException', exitHandler);
} catch (e) {
exit(1);
}

View File

@@ -58,6 +58,13 @@
dependencies:
defer-to-connect "^1.0.1"
"@types/bson@*":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@types/bson/-/bson-4.0.2.tgz#7accb85942fc39bbdb7515d4de437c04f698115f"
integrity sha512-+uWmsejEHfmSjyyM/LkrP0orfE2m5Mx9Xel4tXNeqi1ldK5XMQcDsFkBmLDtuyKUbxj2jGDo0H240fbCRJZo7Q==
dependencies:
"@types/node" "*"
"@types/color-name@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
@@ -75,7 +82,15 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
"@types/node@~14.0.27":
"@types/mongodb@~3.5.25":
version "3.5.25"
resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.5.25.tgz#ab187db04d79f8e3f15af236327dc9139d9d4736"
integrity sha512-2H/Owt+pHCl9YmBOYnXc3VdnxejJEjVdH+QCWL5ZAfPehEn3evygKBX3/vKRv7aTwfNbUd0E5vjJdQklH/9a6w==
dependencies:
"@types/bson" "*"
"@types/node" "*"
"@types/node@*", "@types/node@~14.0.27":
version "14.0.27"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.27.tgz#a151873af5a5e851b51b3b065c9e63390a9e0eb1"
integrity sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g==
@@ -171,6 +186,14 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9"
integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==
bl@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.0.tgz#e1a574cdf528e4053019bb800b041c0ac88da493"
integrity sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA==
dependencies:
readable-stream "^2.3.5"
safe-buffer "^5.1.1"
boxen@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64"
@@ -200,6 +223,11 @@ braces@~3.0.2:
dependencies:
fill-range "^7.0.1"
bson@^1.1.4:
version "1.1.5"
resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.5.tgz#2aaae98fcdf6750c0848b0cba1ddec3c73060a34"
integrity sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
@@ -405,6 +433,11 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
denque@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf"
integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
@@ -801,6 +834,11 @@ make-error@^1.1.1:
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
memory-pager@^1.0.2:
version "1.5.0"
resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5"
integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==
mime-db@1.44.0:
version "1.44.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
@@ -837,6 +875,19 @@ mkdirp@^0.5.3:
dependencies:
minimist "^1.2.5"
mongodb@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.6.0.tgz#babd7172ec717e2ed3f85e079b3f1aa29dce4724"
integrity sha512-/XWWub1mHZVoqEsUppE0GV7u9kanLvHxho6EvBxQbShXTKYF9trhZC2NzbulRGeG7xMJHD8IOWRcdKx5LPjAjQ==
dependencies:
bl "^2.2.0"
bson "^1.1.4"
denque "^1.4.1"
require_optional "^1.0.1"
safe-buffer "^5.1.2"
optionalDependencies:
saslprep "^1.0.0"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -979,7 +1030,7 @@ rc@^1.2.8:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
readable-stream@^2.3.7:
readable-stream@^2.3.5, readable-stream@^2.3.7:
version "2.3.7"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
@@ -1027,6 +1078,19 @@ registry-url@^5.0.0:
dependencies:
rc "^1.2.8"
require_optional@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e"
integrity sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==
dependencies:
resolve-from "^2.0.0"
semver "^5.1.0"
resolve-from@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57"
integrity sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=
resolve@^1.3.2:
version "1.17.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444"
@@ -1041,15 +1105,22 @@ responselike@^1.0.2:
dependencies:
lowercase-keys "^1.0.0"
safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
saslprep@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226"
integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==
dependencies:
sparse-bitfield "^3.0.3"
semver-diff@^3.1.1:
version "3.1.1"
@@ -1058,7 +1129,7 @@ semver-diff@^3.1.1:
dependencies:
semver "^6.3.0"
semver@^5.3.0, semver@^5.7.1:
semver@^5.1.0, semver@^5.3.0, semver@^5.7.1:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@@ -1098,6 +1169,13 @@ source-map@^0.6.0:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
sparse-bitfield@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11"
integrity sha1-/0rm5oZWBWuks+eSqzM004JzyhE=
dependencies:
memory-pager "^1.0.2"
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"