diff --git a/CHANGELOG.md b/CHANGELOG.md index 61d193a..9c255ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [v0.11.2](https://github.com/jovijovi/ether-goblin/releases/tag/v0.11.2) + +### Features + +- (module/event/fetcher/db): add `BulkSave` + +### Performance + +- (module/event/fetcher): improve dump events performance by `BulkSave` + +### Refactor + +- (module/event/fetcher): refactor callback + +### Build + +- Bump packages + ## [v0.11.0](https://github.com/jovijovi/ether-goblin/releases/tag/v0.11.0) ### BREAKING CHANGES diff --git a/conf/app.config.yaml b/conf/app.config.yaml index 0d0c200..72c0824 100644 --- a/conf/app.config.yaml +++ b/conf/app.config.yaml @@ -194,12 +194,12 @@ custom: # Keep running fetcher keepRunning: false - # Force update database if the data already exists - forceUpdate: true - # Supported Database: postgres, mysql or sqlite db: postgres + # Chunk size for saving data to the database + chunkSize: 200 + # NFT contract owners (TODO) contractOwners: - 'CONTRACT_OWNER_ADDRESS_1' diff --git a/devenv/dev.yaml b/devenv/dev.yaml index 9c8caf6..f889645 100644 --- a/devenv/dev.yaml +++ b/devenv/dev.yaml @@ -5,7 +5,7 @@ networks: services: postgres: - container_name: devenv_postgres_pedrojs + container_name: devenv_postgres_goblin image: postgres:13.8 restart: always environment: @@ -19,7 +19,7 @@ services: - devenv-network-ether-goblin mysql: - container_name: devenv_mysql_pedrojs + container_name: devenv_mysql_goblin image: mysql:8.0.26 command: --default-authentication-plugin=mysql_native_password restart: always diff --git a/package.json b/package.json index 1a77b41..242a81c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ether-goblin", - "version": "0.11.0", + "version": "0.11.2", "description": "A microservice for the Ethereum ecosystem.", "author": "jovijovi ", "license": "MIT", @@ -38,10 +38,10 @@ "@jovijovi/express-2fa-token": "^1.1.0", "@jovijovi/pedrojs-common": "1.1.22", "@jovijovi/pedrojs-loader": "^1.1.23", - "@jovijovi/pedrojs-mysql": "1.1.22", + "@jovijovi/pedrojs-mysql": "1.1.23", "@jovijovi/pedrojs-network-http": "1.1.23", - "@jovijovi/pedrojs-pg": "1.1.22", - "@jovijovi/pedrojs-sqlite": "1.1.22", + "@jovijovi/pedrojs-pg": "1.1.23", + "@jovijovi/pedrojs-sqlite": "1.1.23", "@openzeppelin/contracts": "4.7.3", "@types/progress": "^2.0.5", "ethers": "5.7.1", diff --git a/src/config/config.ts b/src/config/config.ts index 263fbd7..14b63d1 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -70,8 +70,8 @@ export namespace customConfig { pushJobIntervals?: number executeJobConcurrency?: number keepRunning?: boolean - forceUpdate?: boolean db: string + chunkSize: number contractOwners: string[] } diff --git a/src/module/event/common/types.ts b/src/module/event/common/types.ts index d89a5ee..375ffec 100644 --- a/src/module/event/common/types.ts +++ b/src/module/event/common/types.ts @@ -4,6 +4,7 @@ export type EventTransfer = { blockNumber: number // Block number blockHash: string // Block hash blockTimestamp?: number // Block timestamp + blockDatetime?: string // Block datetime transactionHash: string // Tx hash from: string // From to: string // To diff --git a/src/module/event/fetcher/callback.ts b/src/module/event/fetcher/callback.ts new file mode 100644 index 0000000..047bc4f --- /dev/null +++ b/src/module/event/fetcher/callback.ts @@ -0,0 +1,62 @@ +import got from 'got'; +import fastq, {queueAsPromised} from 'fastq'; +import {auditor, log, util} from '@jovijovi/pedrojs-common'; +import {customConfig} from '../../../config'; +import {EventTransfer, Response} from '../common/types'; +import {DefaultLoopInterval} from './params'; + +// Callback queue (ASC, FIFO) +const callbackQueue = new util.Queue(); + +// Callback job +const callbackJob: queueAsPromised> = fastq.promise(callback, 1); + +// Schedule processing job +export async function Run() { + let isEmpty = true; + setInterval(() => { + auditor.Check(callbackQueue, "Callback queue is nil"); + if (callbackQueue.Length() === 0) { + if (!isEmpty) { + log.RequestId().info("All callback finished, queue is empty"); + isEmpty = true; + } + return; + } + + callbackJob.push(callbackQueue).catch((err) => log.RequestId().error(err)); + isEmpty = false; + }, DefaultLoopInterval); +} + +// Event callback +async function callback(queue: util.Queue): Promise { + try { + const conf = customConfig.GetEvents().fetcher; + // Check URL + if (!conf.callback) { + return; + } + + const len = queue.Length(); + if (len === 0) { + return; + } + + for (let i = 0; i < len; i++) { + const evt = queue.Shift(); + + // Callback + log.RequestId().debug("Fetcher calling back(%s)... event:", conf.callback, evt); + const rsp: Response = await got.post(conf.callback, { + json: evt + }).json(); + + log.RequestId().trace("Fetcher callback response=", rsp); + } + } catch (e) { + log.RequestId().error("Fetcher callback failed, error=", e); + } + + return; +} diff --git a/src/module/event/fetcher/db/interface.ts b/src/module/event/fetcher/db/interface.ts index 5f86a7b..7a398e0 100644 --- a/src/module/event/fetcher/db/interface.ts +++ b/src/module/event/fetcher/db/interface.ts @@ -1,5 +1,5 @@ import {ModelCtor} from 'sequelize'; -import {log, util} from '@jovijovi/pedrojs-common'; +import {log} from '@jovijovi/pedrojs-common'; import {EventTransfer} from '../../common/types'; import {IMintEvents} from './model'; import {IQuery} from './types'; @@ -22,7 +22,7 @@ export class Database implements IDatabase { block_number: evt.blockNumber.toString(), // Block number block_hash: evt.blockHash, // Block hash block_timestamp: evt.blockTimestamp, // Block timestamp - block_datetime: util.time.GetUnixTimestamp(evt.blockTimestamp, 'UTC'), // Block datetime + block_datetime: evt.blockDatetime, // Block datetime transaction_hash: evt.transactionHash, // Tx hash from: evt.from, // From to: evt.to, // To @@ -37,6 +37,19 @@ export class Database implements IDatabase { return; } + // Save records in bulk, ignore duplicates + async BulkSave(records: any[]): Promise { + try { + await this.ModelEvent.bulkCreate(records, + { + ignoreDuplicates: true, + } + ); + } catch (e) { + log.RequestId().error('BulkSave failed, error=', e.message); + } + } + // Check if exists async IsExists(query: IQuery): Promise { try { @@ -53,7 +66,7 @@ export class Database implements IDatabase { } } - // Query token history (all event type) + // Query token history(all event types) order by 'block_number' ASC async QueryTokenHistory(address: string, tokenId: string): Promise { try { return await this.ModelEvent.findAll( @@ -71,5 +84,3 @@ export class Database implements IDatabase { } } } - - diff --git a/src/module/event/fetcher/dump.ts b/src/module/event/fetcher/dump.ts new file mode 100644 index 0000000..e2d812d --- /dev/null +++ b/src/module/event/fetcher/dump.ts @@ -0,0 +1,92 @@ +import fastq, {queueAsPromised} from 'fastq'; +import {auditor, log, util} from '@jovijovi/pedrojs-common'; +import {EventTransfer} from '../common/types'; +import {customConfig} from '../../../config'; +import {DefaultChunkSize, DefaultLoopInterval} from './params'; +import {DB} from './db'; +import {NewJobID} from '../utils'; + +// Event queue (ASC, FIFO) +const eventQueue = new util.Queue(); + +// Dump job +const dumpJob: queueAsPromised> = fastq.promise(dump, 1); + +// Schedule dump job +export async function Run() { + let isEmpty = true; + setInterval(() => { + auditor.Check(eventQueue, "Event queue is nil"); + if (eventQueue.Length() === 0) { + if (!isEmpty) { + log.RequestId().info("All events dumped, queue is empty"); + isEmpty = true; + } + return; + } + + dumpJob.push(eventQueue).catch((err) => log.RequestId().error(err)); + isEmpty = false; + }, DefaultLoopInterval); +} + +export function Push(evt: EventTransfer) { + eventQueue.Push(evt); +} + +// Dump events +async function dump(queue: util.Queue): Promise { + try { + const len = queue.Length(); + if (len === 0) { + return; + } + + const conf = customConfig.GetEvents().fetcher; + const defaultChunkSize = conf.chunkSize ? conf.chunkSize : DefaultChunkSize; + const jobId = NewJobID(); + + let leftEvents = len; + do { + const chunkSize = leftEvents < defaultChunkSize ? leftEvents : defaultChunkSize; + + const events = []; + for (let i = 0; i < chunkSize; i++) { + const evt = queue.Shift(); + + events.push({ + address: evt.address, // NFT Contract address + block_number: evt.blockNumber.toString(), // Block number + block_hash: evt.blockHash, // Block hash + block_timestamp: evt.blockTimestamp, // Block timestamp + block_datetime: evt.blockDatetime, // Block datetime + transaction_hash: evt.transactionHash, // Tx hash + from: evt.from, // From + to: evt.to, // To + token_id: evt.tokenId.toString(), // NFT Token ID + event_type: evt.eventType // Event type + }); + } + + // Save events in bulk + await DB.Client().BulkSave(events); + + // Calc left events + leftEvents -= chunkSize; + + log.RequestId().debug("EXEC JOB(Dump|id:%s), %d events dumped, progress=%d%(%d/%d), lastBlockInChunk=%s", + jobId, + events.length, + ((len - leftEvents) * 100 / len).toFixed(1), + len - leftEvents, + len, + events[chunkSize - 1].block_number, + ); + } while (leftEvents > 0); + } catch (e) { + log.RequestId().error("Dump failed, error=", e); + return; + } + + return; +} diff --git a/src/module/event/fetcher/fetcher.ts b/src/module/event/fetcher/fetcher.ts index 0984b6d..ce323c8 100644 --- a/src/module/event/fetcher/fetcher.ts +++ b/src/module/event/fetcher/fetcher.ts @@ -2,40 +2,34 @@ import {utils} from 'ethers'; import {Log} from '@ethersproject/abstract-provider'; import {auditor, log, util} from '@jovijovi/pedrojs-common'; import fastq, {queueAsPromised} from 'fastq'; -import got from 'got'; import {network} from '@jovijovi/ether-network'; import {Options} from './options'; import { DefaultExecuteJobConcurrency, DefaultFromBlock, DefaultKeepRunning, - DefaultLoopInterval, DefaultMaxBlockRange, DefaultPushJobIntervals, DefaultQueryIntervals, DefaultRetryTimes, } from './params'; -import {EventTransfer, Response} from '../common/types'; +import {EventTransfer} from '../common/types'; import {customConfig} from '../../../config'; import {DB} from './db'; import {EventMapper, EventNameMapper, EventTypeBurn, EventTypeMint, EventTypeTransfer} from '../common/constants'; -import {CheckEventType, CheckTopics, GetEventType} from '../utils'; +import {CheckEventType, CheckTopics, GetEventType, NewJobID} from '../utils'; import {NewProgressBar, UpdateProgressBar} from './progress'; +import * as callback from './callback'; +import * as dump from './dump'; import {GetBlockNumber, GetBlockTimestamp, RandomRetryInterval} from './common'; import {Cache} from '../../../common/cache'; -// Event queue (ASC, FIFO) -const eventQueue = new util.Queue(); - // Fetch events jobs const fetchEventsJobs: queueAsPromised = fastq.promise(fetchEvents, 1); // Query logs jobs let queryLogsJobs: queueAsPromised; -// Dump job -const dumpJob: queueAsPromised> = fastq.promise(dump, 1); - // Run event fetcher export async function Run() { const [conf, ok] = await init(); @@ -43,15 +37,13 @@ export async function Run() { return; } - // Schedule processing job - setInterval(() => { - auditor.Check(eventQueue, "Event queue is nil"); - if (eventQueue.Length() === 0) { - return; - } + // Schedule dump job + await dump.Run(); - dumpJob.push(eventQueue).catch((err) => log.RequestId().error(err)); - }, DefaultLoopInterval); + // Schedule callback job + if (conf.callback) { + await callback.Run(); + } // Push FetchEvents job if (!conf.api) { @@ -102,8 +94,8 @@ async function queryLogs(opts: Options = { eventType: [EventTypeMint, EventTypeTransfer, EventTypeBurn], fromBlock: DefaultFromBlock }): Promise { - log.RequestId().trace("EXEC JOB(%s), QueryLogs(blocks[%d,%d]) running... QueryLogsJobsCount=%d", - opts.eventType, opts.fromBlock, opts.toBlock, queryLogsJobs.length()); + log.RequestId().trace("EXEC JOB(QueryLogs|id:%s), blocks[%d,%d], TotalJobs=%d", + opts.jobId, opts.fromBlock, opts.toBlock, queryLogsJobs.length()); // Get topic ID (string array) const eventFragments = opts.eventType.map(x => EventMapper.get(x)); @@ -137,11 +129,13 @@ async function queryLogs(opts: Options = { } // Build event + const blockTimestamp = await GetBlockTimestamp(event.blockHash); const evt: EventTransfer = { address: event.address, blockNumber: event.blockNumber, blockHash: event.blockHash, - blockTimestamp: await GetBlockTimestamp(event.blockHash), + blockTimestamp: blockTimestamp, + blockDatetime: util.time.GetUnixTimestamp(blockTimestamp, 'UTC'), transactionHash: event.transactionHash, from: utils.hexZeroPad(utils.hexValue(event.topics[1]), 20), to: utils.hexZeroPad(utils.hexValue(event.topics[2]), 20), @@ -150,11 +144,11 @@ async function queryLogs(opts: Options = { }; // Push event to queue - eventQueue.Push(evt); + dump.Push(evt); } - log.RequestId().trace("JOB(%s) FINISHED, QueryLogs(blocks[%d,%d]), QueryLogsJobsCount=%d", - opts.eventType, opts.fromBlock, opts.toBlock, queryLogsJobs.length()); + log.RequestId().trace("FINISHED JOB(QueryLogs|id:%s), blocks[%d,%d], TotalJobs=%d", + opts.jobId, opts.fromBlock, opts.toBlock, queryLogsJobs.length()); return; } @@ -199,14 +193,17 @@ async function fetchEvents(opts: Options = { if (blockRange >= 0 && blockRange <= 1) { log.RequestId().debug("Catch up the latest block(%d)", blockNumber); } - log.RequestId().trace("PUSH JOB, blocks[%d,%d](range=%d), queryLogsJobs=%d", nextFrom, nextTo, blockRange, queryLogsJobs.length()); - queryLogsJobs.push({ + const jobOpts = { + jobId: NewJobID(), // Job ID address: opts.address, // The address to filter by, or null to match any address eventType: opts.eventType, // ERC721 event type: mint/transfer/burn fromBlock: nextFrom, // Fetch from block number toBlock: nextTo, // Fetch to block number - }).catch((err) => log.RequestId().error(err)); + } + log.RequestId().trace("PUSH JOB(QueryLogs|id:%s), blocks[%d,%d](range=%d), TotalJobs=%d", + jobOpts.jobId, nextFrom, nextTo, blockRange, queryLogsJobs.length()); + queryLogsJobs.push(jobOpts).catch((err) => log.RequestId().error(err)); // Update progress UpdateProgressBar(progress, nextTo - nextFrom); @@ -214,69 +211,7 @@ async function fetchEvents(opts: Options = { nextFrom = nextTo + 1; } while (nextFrom > 0); - log.RequestId().info("FetchEvents finished, options=%o", opts); - - return; -} - -// Event callback -async function callback(evt: EventTransfer): Promise { - try { - const conf = customConfig.GetEvents().fetcher; - - // Check URL - if (!conf.callback) { - return; - } - - // Callback - log.RequestId().debug("Fetcher calling back(%s)... event:", conf.callback, evt); - const rsp: Response = await got.post(conf.callback, { - json: evt - }).json(); - - log.RequestId().trace("Fetcher callback response=", rsp); - } catch (e) { - log.RequestId().error("Fetcher callback failed, error=", e); - return; - } - - return; -} - -// Dump events -async function dump(queue: util.Queue): Promise { - try { - const conf = customConfig.GetEvents().fetcher; - const len = queue.Length(); - if (len === 0) { - return; - } - - for (let i = 0; i < len; i++) { - const evt = queue.Shift(); - - // Callback (Optional) - await callback(evt); - - // Dump event to database - if (!conf.forceUpdate && await DB.Client().IsExists({ - address: evt.address, - blockNumber: evt.blockNumber, - transactionHash: evt.transactionHash, - tokenId: evt.tokenId.toString(), - })) { - log.RequestId().trace("Token(%s) in block(%d) tx(%s) already exists, skipped", - evt.tokenId.toString(), evt.blockNumber, evt.transactionHash); - return; - } - log.RequestId().info("Dumping events to db, count=%d, event=%o", i + 1, evt); - await DB.Client().Save(evt); - } - } catch (e) { - log.RequestId().error("Dump failed, error=", e); - return; - } + log.RequestId().info("All JOBs(QueryLogs) are scheduled, options=%o", opts); return; } diff --git a/src/module/event/fetcher/options.ts b/src/module/event/fetcher/options.ts index ab4c91e..337307c 100644 --- a/src/module/event/fetcher/options.ts +++ b/src/module/event/fetcher/options.ts @@ -1,4 +1,5 @@ export type Options = { + jobId?: string // Job ID eventType: string[] // ERC721 event type: mint/transfer/burn abi?: any address?: string // The address to filter by, or null to match any address (Optional) diff --git a/src/module/event/fetcher/params.ts b/src/module/event/fetcher/params.ts index 86930d3..b91b329 100644 --- a/src/module/event/fetcher/params.ts +++ b/src/module/event/fetcher/params.ts @@ -27,3 +27,6 @@ export const DefaultRetryMaxInterval = DefaultRetryMinInterval * 3; // Keep running or not export const DefaultKeepRunning = false; + +// Default chunk length +export const DefaultChunkSize = 200; diff --git a/src/module/event/utils/params.ts b/src/module/event/utils/params.ts new file mode 100644 index 0000000..df61e38 --- /dev/null +++ b/src/module/event/utils/params.ts @@ -0,0 +1,5 @@ +// Default NanoID alphabet +export const DefaultNanoIDAlphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + +// Default job ID length +export const DefaultJobIDLength = 8; diff --git a/src/module/event/utils/utils.ts b/src/module/event/utils/utils.ts index de32f42..2dabeb4 100644 --- a/src/module/event/utils/utils.ts +++ b/src/module/event/utils/utils.ts @@ -1,5 +1,7 @@ import {constants} from 'ethers'; +import {util} from '@jovijovi/pedrojs-common'; import {EventTypeBurn, EventTypeMint, EventTypeTransfer} from '../common/constants'; +import {DefaultJobIDLength, DefaultNanoIDAlphabet} from './params'; // Check if the event topics is ERC721 compliant. export function CheckTopics(topics: Array): boolean { @@ -37,3 +39,11 @@ export function GetEventType(topics: Array): string { return undefined; } + +// New job ID +export function NewJobID(): string { + return util.nanoid.NewNanoID({ + alphabet: DefaultNanoIDAlphabet, + size: DefaultJobIDLength, + }); +} diff --git a/yarn.lock b/yarn.lock index 80b5ec4..ffd0d00 100644 --- a/yarn.lock +++ b/yarn.lock @@ -450,14 +450,14 @@ "@jovijovi/pedrojs-version" "^1.0.12" log4js "6.7.0" -"@jovijovi/pedrojs-mysql@1.1.22": - version "1.1.22" - resolved "https://registry.yarnpkg.com/@jovijovi/pedrojs-mysql/-/pedrojs-mysql-1.1.22.tgz#576650e896cc1df7d47900e773bd346966be2b25" - integrity sha512-+TYz8nQH4Uvuj8Rit92U2j0+M23Qk/MGoQzDEumZWDekkMaqlU/Hb2V8H1o7/hq7bopd7WnuFlkUWk+URrkyJQ== +"@jovijovi/pedrojs-mysql@1.1.23": + version "1.1.23" + resolved "https://registry.yarnpkg.com/@jovijovi/pedrojs-mysql/-/pedrojs-mysql-1.1.23.tgz#a5e54cbc863daeeb5e4a20fb18e706f82716d0fc" + integrity sha512-gjGRxi4zrv+8P6K8Tr9JLfh0g5fgpo+VFmr97nJ690iPEhwwKN3MYqMUHiuNYpdK9DIw02CVyJtRFLcRTVoUZQ== dependencies: "@jovijovi/pedrojs-log" "^1.1.22" mysql2 "^2.3.3" - sequelize "^6.24.0" + sequelize "^6.25.0" "@jovijovi/pedrojs-network-http@1.1.23": version "1.1.23" @@ -472,23 +472,23 @@ express "4.18.2" nanoid "3.3.4" -"@jovijovi/pedrojs-pg@1.1.22": - version "1.1.22" - resolved "https://registry.yarnpkg.com/@jovijovi/pedrojs-pg/-/pedrojs-pg-1.1.22.tgz#6af0e9145fc189cd8eca68892692129d18b929d0" - integrity sha512-NfpnzV8wdGDs1GN7Imtx/4daqNCkahmXuT2DdmVY1PN9rZmNHnL1zPYY1ZxcL2CgmUjInqO42DY+wqHNtiDvVg== +"@jovijovi/pedrojs-pg@1.1.23": + version "1.1.23" + resolved "https://registry.yarnpkg.com/@jovijovi/pedrojs-pg/-/pedrojs-pg-1.1.23.tgz#4e810933c5f1b4997a3592d212644c429c492468" + integrity sha512-8CZ5mc7dVkJmyN+BhMkz65X+QNW2oh1wk7djPCYuGf7fMqjYqzxbG8pt4MmMlSRtKRi3NanNn9kX4BRK5vVmmQ== dependencies: "@jovijovi/pedrojs-log" "^1.1.22" pg "^8.7.3" pg-hstore "^2.3.4" - sequelize "^6.24.0" + sequelize "^6.25.0" -"@jovijovi/pedrojs-sqlite@1.1.22": - version "1.1.22" - resolved "https://registry.yarnpkg.com/@jovijovi/pedrojs-sqlite/-/pedrojs-sqlite-1.1.22.tgz#5bcd43b6bc778d2f5cd0f3a38034030a3c600206" - integrity sha512-1UeWZFocNEhrz3tXkZiL+TRXDoLApSCwg8r2h3yrQmutyy/v6RM1T2hwGJfSgbEJVmNMTxVQQtO3g0NIBZc4CA== +"@jovijovi/pedrojs-sqlite@1.1.23": + version "1.1.23" + resolved "https://registry.yarnpkg.com/@jovijovi/pedrojs-sqlite/-/pedrojs-sqlite-1.1.23.tgz#3ab32eecba7a9037bd83fa5554729dcef7955d40" + integrity sha512-WDGIVk8TSgoIa3F5hihW6ABImpz4Tee5LHH5SyI4/zDcObE9UWO9sZh5VYN9SKDK5MKDHPKxnAqy+xdQMZS+QA== dependencies: "@jovijovi/pedrojs-log" "^1.1.22" - sequelize "^6.24.0" + sequelize "^6.25.0" sqlite3 "5.0.11" "@jovijovi/pedrojs-tracing@1.1.22": @@ -4626,10 +4626,10 @@ sequelize-pool@^7.1.0: resolved "https://registry.yarnpkg.com/sequelize-pool/-/sequelize-pool-7.1.0.tgz#210b391af4002762f823188fd6ecfc7413020768" integrity sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg== -sequelize@^6.24.0: - version "6.24.0" - resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-6.24.0.tgz#ae5c60a54f10c3bdf1c57318a72b224bad306956" - integrity sha512-mPo7Q7gWkrsstjR2aw8ahkrj8RUJCozz3kedAz2B5ZIdLoQEH1z2tvaeJONI8R6RqeuyRQosx9Yn3s9yQyo6lQ== +sequelize@^6.25.0: + version "6.25.0" + resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-6.25.0.tgz#957c2c7a90a5fcd544cf92903715593e49002c8b" + integrity sha512-Bu2V+Yjw/4fwGve0rwk02jmbuA0JGGGD1N7yrfGQXVRD91hg4j9Ecth4NwnK4fX1MQA4R0Cqfx3BaJDAOoaiSA== dependencies: "@types/debug" "^4.1.7" "@types/validator" "^13.7.1"