From c64e862b196ce6db6111920ee2ba5df2a3e5887e Mon Sep 17 00:00:00 2001 From: Morgan McCauley Date: Fri, 4 Aug 2023 16:43:56 +1200 Subject: [PATCH 01/31] DPLT-1076 create node worker to process real time streams (#159) --- indexer/queryapi_coordinator/src/main.rs | 13 +- indexer/storage/src/lib.rs | 30 +- runner/.eslintrc.js | 32 + runner/.gitignore | 2 + runner/jest.config.js | 4 + runner/package-lock.json | 7365 +++++++++++++++++ runner/package.json | 50 + runner/src/globals.d.ts | 12 + .../__snapshots__/hasura-client.test.ts.snap | 406 + .../src/hasura-client/hasura-client.test.ts | 247 + runner/src/hasura-client/hasura-client.ts | 339 + runner/src/hasura-client/index.ts | 1 + runner/src/index.ts | 156 + .../__snapshots__/indexer.test.ts.snap | 327 + runner/src/indexer/index.ts | 1 + runner/src/indexer/indexer.test.ts | 762 ++ runner/src/indexer/indexer.ts | 353 + runner/src/pg-client.ts | 41 + .../__snapshots__/provisioner.test.ts.snap | 18 + runner/src/provisioner/index.ts | 1 + runner/src/provisioner/provisioner.test.ts | 212 + runner/src/provisioner/provisioner.ts | 161 + runner/tsconfig.json | 25 + 23 files changed, 10538 insertions(+), 20 deletions(-) create mode 100644 runner/.eslintrc.js create mode 100644 runner/.gitignore create mode 100644 runner/jest.config.js create mode 100644 runner/package-lock.json create mode 100644 runner/package.json create mode 100644 runner/src/globals.d.ts create mode 100644 runner/src/hasura-client/__snapshots__/hasura-client.test.ts.snap create mode 100644 runner/src/hasura-client/hasura-client.test.ts create mode 100644 runner/src/hasura-client/hasura-client.ts create mode 100644 runner/src/hasura-client/index.ts create mode 100644 runner/src/index.ts create mode 100644 runner/src/indexer/__snapshots__/indexer.test.ts.snap create mode 100644 runner/src/indexer/index.ts create mode 100644 runner/src/indexer/indexer.test.ts create mode 100644 runner/src/indexer/indexer.ts create mode 100644 runner/src/pg-client.ts create mode 100644 runner/src/provisioner/__snapshots__/provisioner.test.ts.snap create mode 100644 runner/src/provisioner/index.ts create mode 100644 runner/src/provisioner/provisioner.test.ts create mode 100644 runner/src/provisioner/provisioner.ts create mode 100644 runner/tsconfig.json diff --git a/indexer/queryapi_coordinator/src/main.rs b/indexer/queryapi_coordinator/src/main.rs index 96cb059d5..770852e2a 100644 --- a/indexer/queryapi_coordinator/src/main.rs +++ b/indexer/queryapi_coordinator/src/main.rs @@ -200,16 +200,21 @@ async fn handle_streamer_message( set_provisioned_flag(&mut indexer_registry_locked, &indexer_function); } + storage::sadd( + context.redis_connection_manager, + storage::INDEXER_SET_KEY, + indexer_function.get_full_name(), + ) + .await?; storage::set( context.redis_connection_manager, - &format!("{}:storage", indexer_function.get_full_name()), + storage::generate_storage_key(&indexer_function.get_full_name()), serde_json::to_string(indexer_function)?, ) .await?; - - storage::add_to_registered_stream( + storage::xadd( context.redis_connection_manager, - &format!("{}:stream", indexer_function.get_full_name()), + storage::generate_stream_key(&indexer_function.get_full_name()), &[("block_height", block_height)], ) .await?; diff --git a/indexer/storage/src/lib.rs b/indexer/storage/src/lib.rs index 98c88a297..3753e186f 100644 --- a/indexer/storage/src/lib.rs +++ b/indexer/storage/src/lib.rs @@ -2,12 +2,20 @@ pub use redis::{self, aio::ConnectionManager, FromRedisValue, ToRedisArgs}; const STORAGE: &str = "storage_alertexer"; -const STREAMS_SET_KEY: &str = "streams"; +pub const INDEXER_SET_KEY: &str = "indexers"; pub async fn get_redis_client(redis_connection_str: &str) -> redis::Client { redis::Client::open(redis_connection_str).expect("can create redis client") } +pub fn generate_storage_key(name: &str) -> String { + format!("{}:storage", name) +} + +pub fn generate_stream_key(name: &str) -> String { + format!("{}:stream", name) +} + pub async fn connect(redis_connection_str: &str) -> anyhow::Result { Ok(get_redis_client(redis_connection_str) .await @@ -53,7 +61,7 @@ pub async fn get( Ok(value) } -async fn sadd( +pub async fn sadd( redis_connection_manager: &ConnectionManager, key: impl ToRedisArgs + std::fmt::Debug, value: impl ToRedisArgs + std::fmt::Debug, @@ -69,16 +77,16 @@ async fn sadd( Ok(()) } -async fn xadd( +pub async fn xadd( redis_connection_manager: &ConnectionManager, - stream_key: &str, + stream_key: impl ToRedisArgs + std::fmt::Debug, fields: &[(&str, impl ToRedisArgs + std::fmt::Debug)], ) -> anyhow::Result<()> { - tracing::debug!(target: STORAGE, "XADD: {}, {:?}", stream_key, fields); + tracing::debug!(target: STORAGE, "XADD: {:?}, {:?}", stream_key, fields); // TODO: Remove stream cap when we finally start processing it redis::cmd("XTRIM") - .arg(stream_key) + .arg(&stream_key) .arg("MAXLEN") .arg(100) .query_async(&mut redis_connection_manager.clone()) @@ -97,16 +105,6 @@ async fn xadd( Ok(()) } -pub async fn add_to_registered_stream( - redis_connection_manager: &ConnectionManager, - key: &str, - fields: &[(&str, impl ToRedisArgs + std::fmt::Debug)], -) -> anyhow::Result<()> { - sadd(redis_connection_manager, STREAMS_SET_KEY, key).await?; - xadd(redis_connection_manager, key, fields).await?; - - Ok(()) -} /// Sets the key `receipt_id: &str` with value `transaction_hash: &str` to the Redis storage. /// Increments the counter `receipts_{transaction_hash}` by one. /// The counter holds how many Receipts related to the Transaction are in watching list diff --git a/runner/.eslintrc.js b/runner/.eslintrc.js new file mode 100644 index 000000000..870c73b76 --- /dev/null +++ b/runner/.eslintrc.js @@ -0,0 +1,32 @@ +module.exports = { + parser: '@typescript-eslint/parser', + env: { + es2021: true, + node: true, + }, + overrides: [ + { + files: ['.eslintrc.js', 'jest.config.js'], + parser: 'espree', + extends: ['standard'], + rules: { + semi: ['error', 'always'], + 'comma-dangle': ['error', 'only-multiline'], + }, + }, + { + files: ['./src/**/*'], + parserOptions: { + project: './tsconfig.json', + }, + extends: [ + 'standard-with-typescript', + ], + rules: { + '@typescript-eslint/semi': ['error', 'always'], + '@typescript-eslint/comma-dangle': ['error', 'only-multiline'], + '@typescript-eslint/strict-boolean-expressions': 'off', + }, + }, + ], +}; diff --git a/runner/.gitignore b/runner/.gitignore new file mode 100644 index 000000000..7f277a13c --- /dev/null +++ b/runner/.gitignore @@ -0,0 +1,2 @@ +**/dist +/node_modules diff --git a/runner/jest.config.js b/runner/jest.config.js new file mode 100644 index 000000000..eef6b07c6 --- /dev/null +++ b/runner/jest.config.js @@ -0,0 +1,4 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node' +}; diff --git a/runner/package-lock.json b/runner/package-lock.json new file mode 100644 index 000000000..e0bd77c3e --- /dev/null +++ b/runner/package-lock.json @@ -0,0 +1,7365 @@ +{ + "name": "redis-handler", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "redis-handler", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@near-lake/primitives": "^0.1.0", + "aws-sdk": "^2.1402.0", + "node-fetch": "^2.6.11", + "pg": "^8.11.1", + "pg-format": "^1.0.4", + "redis": "^4.6.7", + "verror": "^1.10.1", + "vm2": "^3.9.19" + }, + "devDependencies": { + "@types/jest": "^29.5.3", + "@types/node": "^20.3.1", + "@types/node-fetch": "^2.6.4", + "@types/pg": "^8.10.2", + "@types/pg-format": "^1.0.2", + "@types/pluralize": "^0.0.29", + "@types/verror": "^1.10.6", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "eslint": "^8.46.0", + "eslint-config-prettier": "^8.9.0", + "eslint-config-standard-with-typescript": "^37.0.0", + "eslint-plugin-import": "^2.28.0", + "eslint-plugin-n": "^16.0.1", + "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-promise": "^6.1.1", + "jest": "^29.6.2", + "pluralize": "^8.0.0", + "prettier": "^3.0.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "^5.1.6" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", + "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", + "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.9", + "@babel/helper-compilation-targets": "^7.22.9", + "@babel/helper-module-transforms": "^7.22.9", + "@babel/helpers": "^7.22.6", + "@babel/parser": "^7.22.7", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.8", + "@babel/types": "^7.22.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.9.tgz", + "integrity": "sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.9.tgz", + "integrity": "sha512-7qYrNM6HjpnPHJbopxmb8hSPoZ0gsX8IvUS32JGVoy+pU9e5N0nLr1VjJoR6kA4d9dmGLxNYOjeB8sUDal2WMw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.9", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.9", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", + "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.6.tgz", + "integrity": "sha512-YjDs6y/fVOYFV8hAf1rxd1QvR9wJe1pDBZ2AREKq/SDayfPzgk0PBnVuTCE5X1acEpMMNOVUqoe+OwiZGJ+OaA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.6", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.7.tgz", + "integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", + "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", + "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.22.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.8.tgz", + "integrity": "sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.7", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.22.7", + "@babel/types": "^7.22.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", + "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.1.tgz", + "integrity": "sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.46.0.tgz", + "integrity": "sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.6.2.tgz", + "integrity": "sha512-0N0yZof5hi44HAR2pPS+ikJ3nzKNoZdVu8FffRf3wy47I7Dm7etk/3KetMdRUqzVd16V4O2m2ISpNTbnIuqy1w==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.1", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.6.2", + "jest-util": "^29.6.2", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.6.2.tgz", + "integrity": "sha512-Oj+5B+sDMiMWLhPFF+4/DvHOf+U10rgvCLGPHP8Xlsy/7QxS51aU/eBngudHlJXnaWD5EohAgJ4js+T6pa+zOg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.6.2", + "@jest/reporters": "^29.6.2", + "@jest/test-result": "^29.6.2", + "@jest/transform": "^29.6.2", + "@jest/types": "^29.6.1", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.5.0", + "jest-config": "^29.6.2", + "jest-haste-map": "^29.6.2", + "jest-message-util": "^29.6.2", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.6.2", + "jest-resolve-dependencies": "^29.6.2", + "jest-runner": "^29.6.2", + "jest-runtime": "^29.6.2", + "jest-snapshot": "^29.6.2", + "jest-util": "^29.6.2", + "jest-validate": "^29.6.2", + "jest-watcher": "^29.6.2", + "micromatch": "^4.0.4", + "pretty-format": "^29.6.2", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.6.2.tgz", + "integrity": "sha512-AEcW43C7huGd/vogTddNNTDRpO6vQ2zaQNrttvWV18ArBx9Z56h7BIsXkNFJVOO4/kblWEQz30ckw0+L3izc+Q==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.6.2", + "@jest/types": "^29.6.1", + "@types/node": "*", + "jest-mock": "^29.6.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.6.2.tgz", + "integrity": "sha512-m6DrEJxVKjkELTVAztTLyS/7C92Y2b0VYqmDROYKLLALHn8T/04yPs70NADUYPrV3ruI+H3J0iUIuhkjp7vkfg==", + "dev": true, + "dependencies": { + "expect": "^29.6.2", + "jest-snapshot": "^29.6.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.6.2.tgz", + "integrity": "sha512-6zIhM8go3RV2IG4aIZaZbxwpOzz3ZiM23oxAlkquOIole+G6TrbeXnykxWYlqF7kz2HlBjdKtca20x9atkEQYg==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.4.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.6.2.tgz", + "integrity": "sha512-euZDmIlWjm1Z0lJ1D0f7a0/y5Kh/koLFMUBE5SUYWrmy8oNhJpbTBDAP6CxKnadcMLDoDf4waRYCe35cH6G6PA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.1", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.6.2", + "jest-mock": "^29.6.2", + "jest-util": "^29.6.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.6.2.tgz", + "integrity": "sha512-cjuJmNDjs6aMijCmSa1g2TNG4Lby/AeU7/02VtpW+SLcZXzOLK2GpN2nLqcFjmhy3B3AoPeQVx7BnyOf681bAw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.6.2", + "@jest/expect": "^29.6.2", + "@jest/types": "^29.6.1", + "jest-mock": "^29.6.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.6.2.tgz", + "integrity": "sha512-sWtijrvIav8LgfJZlrGCdN0nP2EWbakglJY49J1Y5QihcQLfy7ovyxxjJBRXMNltgt4uPtEcFmIMbVshEDfFWw==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.6.2", + "@jest/test-result": "^29.6.2", + "@jest/transform": "^29.6.2", + "@jest/types": "^29.6.1", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.6.2", + "jest-util": "^29.6.2", + "jest-worker": "^29.6.2", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jest/reporters/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.0.tgz", + "integrity": "sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.0.tgz", + "integrity": "sha512-oA+I2SHHQGxDCZpbrsCQSoMLb3Bz547JnM+jUr9qEbuw0vQlWZfpPS7CO9J7XiwKicEz9OFn/IYoLkkiUD7bzA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map/node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jest/source-map/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jest/source-map/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jest/test-result": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.6.2.tgz", + "integrity": "sha512-3VKFXzcV42EYhMCsJQURptSqnyjqCGbtLuX5Xxb6Pm6gUf1wIRIl+mandIRGJyWKgNKYF9cnstti6Ls5ekduqw==", + "dev": true, + "dependencies": { + "@jest/console": "^29.6.2", + "@jest/types": "^29.6.1", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.6.2.tgz", + "integrity": "sha512-GVYi6PfPwVejO7slw6IDO0qKVum5jtrJ3KoLGbgBWyr2qr4GaxFV6su+ZAjdTX75Sr1DkMFRk09r2ZVa+wtCGw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.6.2", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.6.2", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.6.2.tgz", + "integrity": "sha512-ZqCqEISr58Ce3U+buNFJYUktLJZOggfyvR+bZMaiV1e8B1SIvJbwZMrYz3gx/KAPn9EXmOmN+uB08yLCjWkQQg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.1", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.6.2", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.6.2", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jest/transform/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jest/transform/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jest/types": { + "version": "29.6.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.1.tgz", + "integrity": "sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.0", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@near-lake/primitives": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@near-lake/primitives/-/primitives-0.1.0.tgz", + "integrity": "sha512-SvL6mA0SsqAz5AC2811I+cI9Mpayax8VsoRbY0Bizk5eYiGCT1u1iBBa8f1nikquDfJCEK+sBCt751Nz/xoZjw==" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/utils": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", + "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "fast-glob": "^3.3.0", + "is-glob": "^4.0.3", + "open": "^9.1.0", + "picocolors": "^1.0.0", + "tslib": "^2.6.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@pkgr/utils/node_modules/tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==", + "dev": true + }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/client": { + "version": "1.5.8", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.8.tgz", + "integrity": "sha512-xzElwHIO6rBAqzPeVnCzgvrnBEcFL1P0w8P65VNLRkdVW8rOE58f52hdj0BDgmsdOm4f1EoXPZtH4Fh7M/qUpw==", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/graph": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz", + "integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz", + "integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.3.tgz", + "integrity": "sha512-4Dg1JjvCevdiCBTZqjhKkGoC5/BcB7k9j99kdMnaXFXg8x4eyOIVg9487CMv7/BUVkFLZCaIh8ead9mU15DNng==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz", + "integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", + "integrity": "sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.1.tgz", + "integrity": "sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", + "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.3", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.3.tgz", + "integrity": "sha512-1Nq7YrO/vJE/FYnqYyw0FS8LdrjExSgIiHyKg7xPpn+yi8Q4huZryKnkJatN1ZRH89Kw2v33/8ZMB7DuZeSLlA==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz", + "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==", + "dev": true + }, + "node_modules/@types/node-fetch": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.4.tgz", + "integrity": "sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "node_modules/@types/pg": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.2.tgz", + "integrity": "sha512-MKFs9P6nJ+LAeHLU3V0cODEOgyThJ3OAnmOlsZsxux6sfQs3HRXR5bBn7xG5DjckEFhTAxsXi7k7cd0pCMxpJw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" + } + }, + "node_modules/@types/pg-format": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/pg-format/-/pg-format-1.0.2.tgz", + "integrity": "sha512-D3MEO6u3BObw3G4Xewjdx05MF5v/fiog78CedtrXe8BhONM8GvUz2dPfLWtI0BPRBoRd6anPHXe+sbrPReZouQ==", + "dev": true + }, + "node_modules/@types/pg/node_modules/pg-types": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.1.tgz", + "integrity": "sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==", + "dev": true, + "dependencies": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.0.1", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/pg/node_modules/postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dev": true, + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/pg/node_modules/postgres-date": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.0.1.tgz", + "integrity": "sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pg/node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/pluralize": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/pluralize/-/pluralize-0.0.29.tgz", + "integrity": "sha512-BYOID+l2Aco2nBik+iYS4SZX0Lf20KPILP5RGmM1IgzdwNdTs0eebiFriOPcej1sX9mLnSoiNte5zcFxssgpGA==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "dev": true + }, + "node_modules/@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "dev": true + }, + "node_modules/@types/verror": { + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.6.tgz", + "integrity": "sha512-NNm+gdePAX1VGvPcGZCDKQZKYSiAWigKhKaz5KF94hG6f2s8de9Ow5+7AbXoeKxL8gavZfk4UquSAygOF2duEQ==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", + "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", + "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.2.tgz", + "integrity": "sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", + "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", + "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz", + "integrity": "sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sdk": { + "version": "2.1402.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1402.0.tgz", + "integrity": "sha512-ndyx3H+crHPIXJF+SO1dqzzBmQwMdoB9uCND/Ip4Ozfv5jse7X58LWpWucM9KBctQ8o37c8KvXjfTr14lE3ykA==", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.5.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/babel-jest": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.6.2.tgz", + "integrity": "sha512-BYCzImLos6J3BH/+HvUCHG1dTf2MzmAB4jaVxHV+29RZLjR29XuYTmsf2sdDwkrb+FczkGo3kOhE7ga6sI0P4A==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.6.2", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.5.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz", + "integrity": "sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz", + "integrity": "sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.5.0", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "dev": true, + "dependencies": { + "big-integer": "^1.6.44" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.10", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", + "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001517", + "electron-to-chromium": "^1.4.477", + "node-releases": "^2.0.13", + "update-browserslist-db": "^1.0.11" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/builtins": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", + "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", + "dev": true, + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/bundle-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", + "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "dev": true, + "dependencies": { + "run-applescript": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001518", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001518.tgz", + "integrity": "sha512-rup09/e3I0BKjncL+FesTayKtPrdwKhUufQFd3riFw1hHg8JmIFoInYfB102cFcY/pPgGmdyl/iy+jgiDi2vdA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", + "dev": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", + "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "dev": true, + "dependencies": { + "bundle-name": "^3.0.0", + "default-browser-id": "^3.0.0", + "execa": "^7.1.1", + "titleize": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", + "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "dev": true, + "dependencies": { + "bplist-parser": "^0.2.0", + "untildify": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dev": true, + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", + "integrity": "sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.478", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.478.tgz", + "integrity": "sha512-qjTA8djMXd+ruoODDFGnRCRBpID+AAfYWCyGtYTNhsuwxI19s8q19gbjKTwRS5z/LyVf5wICaIiPQGLekmbJbA==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.1.tgz", + "integrity": "sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.1", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.2.1", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.0", + "safe-array-concat": "^1.0.0", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz", + "integrity": "sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.1", + "@eslint/js": "^8.46.0", + "@humanwhocodes/config-array": "^0.11.10", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.2", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.9.0.tgz", + "integrity": "sha512-+sbni7NfVXnOpnRadUA8S28AUlsZt9GjgFvABIRL9Hkn8KqNzOp+7Lw4QWtrwn20KzU3wqu1QoOj2m+7rKRqkA==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-config-standard": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", + "integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": "^8.0.1", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", + "eslint-plugin-promise": "^6.0.0" + } + }, + "node_modules/eslint-config-standard-with-typescript": { + "version": "37.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard-with-typescript/-/eslint-config-standard-with-typescript-37.0.0.tgz", + "integrity": "sha512-V8I/Q1eFf9tiOuFHkbksUdWO3p1crFmewecfBtRxXdnvb71BCJx+1xAknlIRZMwZioMX3/bPtMVCZsf1+AjjOw==", + "dev": true, + "dependencies": { + "@typescript-eslint/parser": "^5.52.0", + "eslint-config-standard": "17.1.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.52.0", + "eslint": "^8.0.1", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", + "eslint-plugin-promise": "^6.0.0", + "typescript": "*" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", + "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.11.0", + "resolve": "^1.22.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-es-x": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.2.0.tgz", + "integrity": "sha512-9dvv5CcvNjSJPqnS5uZkqb3xmbeqRLnvXKK7iI5+oK/yTusyc46zbBZKENGsOfojm/mKfszyZb+wNqNPAPeGXA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.1.2", + "@eslint-community/regexpp": "^4.6.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": ">=8" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.0.tgz", + "integrity": "sha512-B8s/n+ZluN7sxj9eUf7/pRFERX0r5bnFA2dCaLHy2ZeaQEAz0k+ZZkFWRFHJAqxfxQDx6KLv9LeIki7cFdwW+Q==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.findlastindex": "^1.2.2", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.8.0", + "has": "^1.0.3", + "is-core-module": "^2.12.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.6", + "object.groupby": "^1.0.0", + "object.values": "^1.1.6", + "resolve": "^1.22.3", + "semver": "^6.3.1", + "tsconfig-paths": "^3.14.2" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-n": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.0.1.tgz", + "integrity": "sha512-CDmHegJN0OF3L5cz5tATH84RPQm9kG+Yx39wIqIwPR2C0uhBGMWfbbOtetR83PQjjidA5aXMu+LEFw1jaSwvTA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "builtins": "^5.0.1", + "eslint-plugin-es-x": "^7.1.0", + "ignore": "^5.2.4", + "is-core-module": "^2.12.1", + "minimatch": "^3.1.2", + "resolve": "^1.22.2", + "semver": "^7.5.3" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.0.tgz", + "integrity": "sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==", + "dev": true, + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.8.5" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-promise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz", + "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz", + "integrity": "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.6.2.tgz", + "integrity": "sha512-iAErsLxJ8C+S02QbLAwgSGSezLQK+XXRDt8IuFXFpwCNw2ECmzZSmjKcCaFVp5VRMk+WAvz6h6jokzEzBFZEuA==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.6.2", + "@types/node": "*", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.6.2", + "jest-message-util": "^29.6.2", + "jest-util": "^29.6.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-wsl/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.6.2.tgz", + "integrity": "sha512-8eQg2mqFbaP7CwfsTpCxQ+sHzw1WuNWL5UUvjnWP4hx2riGz9fPSzYOaU5q8/GqWn1TfgZIVTqYJygbGbWAANg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.6.2", + "@jest/types": "^29.6.1", + "import-local": "^3.0.2", + "jest-cli": "^29.6.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.5.0.tgz", + "integrity": "sha512-IFG34IUMUaNBIxjQXF/iu7g6EcdMrGRRxaUSw92I/2g2YC6vCdTltl4nHvt7Ci5nSJwXIkCu8Ka1DKF+X7Z1Ag==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-changed-files/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/jest-changed-files/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/jest-changed-files/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-changed-files/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-changed-files/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-circus": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.6.2.tgz", + "integrity": "sha512-G9mN+KOYIUe2sB9kpJkO9Bk18J4dTDArNFPwoZ7WKHKel55eKIS/u2bLthxgojwlf9NLCVQfgzM/WsOVvoC6Fw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.6.2", + "@jest/expect": "^29.6.2", + "@jest/test-result": "^29.6.2", + "@jest/types": "^29.6.1", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.6.2", + "jest-matcher-utils": "^29.6.2", + "jest-message-util": "^29.6.2", + "jest-runtime": "^29.6.2", + "jest-snapshot": "^29.6.2", + "jest-util": "^29.6.2", + "p-limit": "^3.1.0", + "pretty-format": "^29.6.2", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.6.2.tgz", + "integrity": "sha512-TT6O247v6dCEX2UGHGyflMpxhnrL0DNqP2fRTKYm3nJJpCTfXX3GCMQPGFjXDoj0i5/Blp3jriKXFgdfmbYB6Q==", + "dev": true, + "dependencies": { + "@jest/core": "^29.6.2", + "@jest/test-result": "^29.6.2", + "@jest/types": "^29.6.1", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^29.6.2", + "jest-util": "^29.6.2", + "jest-validate": "^29.6.2", + "prompts": "^2.0.1", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.6.2.tgz", + "integrity": "sha512-VxwFOC8gkiJbuodG9CPtMRjBUNZEHxwfQXmIudSTzFWxaci3Qub1ddTRbFNQlD/zUeaifLndh/eDccFX4wCMQw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.6.2", + "@jest/types": "^29.6.1", + "babel-jest": "^29.6.2", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.6.2", + "jest-environment-node": "^29.6.2", + "jest-get-type": "^29.4.3", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.6.2", + "jest-runner": "^29.6.2", + "jest-util": "^29.6.2", + "jest-validate": "^29.6.2", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.6.2", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.6.2.tgz", + "integrity": "sha512-t+ST7CB9GX5F2xKwhwCf0TAR17uNDiaPTZnVymP9lw0lssa9vG+AFyDZoeIHStU3WowFFwT+ky+er0WVl2yGhA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.4.3", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.6.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.4.3.tgz", + "integrity": "sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.6.2.tgz", + "integrity": "sha512-MsrsqA0Ia99cIpABBc3izS1ZYoYfhIy0NNWqPSE0YXbQjwchyt6B1HD2khzyPe1WiJA7hbxXy77ZoUQxn8UlSw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.1", + "chalk": "^4.0.0", + "jest-get-type": "^29.4.3", + "jest-util": "^29.6.2", + "pretty-format": "^29.6.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.6.2.tgz", + "integrity": "sha512-YGdFeZ3T9a+/612c5mTQIllvWkddPbYcN2v95ZH24oWMbGA4GGS2XdIF92QMhUhvrjjuQWYgUGW2zawOyH63MQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.6.2", + "@jest/fake-timers": "^29.6.2", + "@jest/types": "^29.6.1", + "@types/node": "*", + "jest-mock": "^29.6.2", + "jest-util": "^29.6.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.4.3.tgz", + "integrity": "sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.6.2.tgz", + "integrity": "sha512-+51XleTDAAysvU8rT6AnS1ZJ+WHVNqhj1k6nTvN2PYP+HjU3kqlaKQ1Lnw3NYW3bm2r8vq82X0Z1nDDHZMzHVA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.1", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.4.3", + "jest-util": "^29.6.2", + "jest-worker": "^29.6.2", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.6.2.tgz", + "integrity": "sha512-aNqYhfp5uYEO3tdWMb2bfWv6f0b4I0LOxVRpnRLAeque2uqOVVMLh6khnTcE2qJ5wAKop0HcreM1btoysD6bPQ==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.4.3", + "pretty-format": "^29.6.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.6.2.tgz", + "integrity": "sha512-4LiAk3hSSobtomeIAzFTe+N8kL6z0JtF3n6I4fg29iIW7tt99R7ZcIFW34QkX+DuVrf+CUe6wuVOpm7ZKFJzZQ==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.6.2", + "jest-get-type": "^29.4.3", + "pretty-format": "^29.6.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.6.2.tgz", + "integrity": "sha512-vnIGYEjoPSuRqV8W9t+Wow95SDp6KPX2Uf7EoeG9G99J2OVh7OSwpS4B6J0NfpEIpfkBNHlBZpA2rblEuEFhZQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.1", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.6.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.6.2.tgz", + "integrity": "sha512-hoSv3lb3byzdKfwqCuT6uTscan471GUECqgNYykg6ob0yiAw3zYc7OrPnI9Qv8Wwoa4lC7AZ9hyS4AiIx5U2zg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.1", + "@types/node": "*", + "jest-util": "^29.6.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.4.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz", + "integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.6.2.tgz", + "integrity": "sha512-G/iQUvZWI5e3SMFssc4ug4dH0aZiZpsDq9o1PtXTV1210Ztyb2+w+ZgQkB3iOiC5SmAEzJBOHWz6Hvrd+QnNPw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.6.2", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.6.2", + "jest-validate": "^29.6.2", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.6.2.tgz", + "integrity": "sha512-LGqjDWxg2fuQQm7ypDxduLu/m4+4Lb4gczc13v51VMZbVP5tSBILqVx8qfWcsdP8f0G7aIqByIALDB0R93yL+w==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.4.3", + "jest-snapshot": "^29.6.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.6.2.tgz", + "integrity": "sha512-wXOT/a0EspYgfMiYHxwGLPCZfC0c38MivAlb2lMEAlwHINKemrttu1uSbcGbfDV31sFaPWnWJPmb2qXM8pqZ4w==", + "dev": true, + "dependencies": { + "@jest/console": "^29.6.2", + "@jest/environment": "^29.6.2", + "@jest/test-result": "^29.6.2", + "@jest/transform": "^29.6.2", + "@jest/types": "^29.6.1", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.4.3", + "jest-environment-node": "^29.6.2", + "jest-haste-map": "^29.6.2", + "jest-leak-detector": "^29.6.2", + "jest-message-util": "^29.6.2", + "jest-resolve": "^29.6.2", + "jest-runtime": "^29.6.2", + "jest-util": "^29.6.2", + "jest-watcher": "^29.6.2", + "jest-worker": "^29.6.2", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.6.2.tgz", + "integrity": "sha512-2X9dqK768KufGJyIeLmIzToDmsN0m7Iek8QNxRSI/2+iPFYHF0jTwlO3ftn7gdKd98G/VQw9XJCk77rbTGZnJg==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.6.2", + "@jest/fake-timers": "^29.6.2", + "@jest/globals": "^29.6.2", + "@jest/source-map": "^29.6.0", + "@jest/test-result": "^29.6.2", + "@jest/transform": "^29.6.2", + "@jest/types": "^29.6.1", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.6.2", + "jest-message-util": "^29.6.2", + "jest-mock": "^29.6.2", + "jest-regex-util": "^29.4.3", + "jest-resolve": "^29.6.2", + "jest-snapshot": "^29.6.2", + "jest-util": "^29.6.2", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.6.2.tgz", + "integrity": "sha512-1OdjqvqmRdGNvWXr/YZHuyhh5DeaLp1p/F8Tht/MrMw4Kr1Uu/j4lRG+iKl1DAqUJDWxtQBMk41Lnf/JETYBRA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.6.2", + "@jest/transform": "^29.6.2", + "@jest/types": "^29.6.1", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.6.2", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.6.2", + "jest-get-type": "^29.4.3", + "jest-matcher-utils": "^29.6.2", + "jest-message-util": "^29.6.2", + "jest-util": "^29.6.2", + "natural-compare": "^1.4.0", + "pretty-format": "^29.6.2", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.6.2.tgz", + "integrity": "sha512-3eX1qb6L88lJNCFlEADKOkjpXJQyZRiavX1INZ4tRnrBVr2COd3RgcTLyUiEXMNBlDU/cgYq6taUS0fExrWW4w==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.1", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.6.2.tgz", + "integrity": "sha512-vGz0yMN5fUFRRbpJDPwxMpgSXW1LDKROHfBopAvDcmD6s+B/s8WJrwi+4bfH4SdInBA5C3P3BI19dBtKzx1Arg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.1", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.4.3", + "leven": "^3.1.0", + "pretty-format": "^29.6.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.6.2.tgz", + "integrity": "sha512-GZitlqkMkhkefjfN/p3SJjrDaxPflqxEAv3/ik10OirZqJGYH5rPiIsgVcfof0Tdqg3shQGdEIxDBx+B4tuLzA==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.6.2", + "@jest/types": "^29.6.1", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.6.2", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.6.2.tgz", + "integrity": "sha512-l3ccBOabTdkng8I/ORCkADz4eSMKejTYv1vB/Z83UiubqhC1oQ5Li6dWCyqOIvSifGjUBxuvxvlm6KGK2DtuAQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.6.2", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "node_modules/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", + "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz", + "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.0.tgz", + "integrity": "sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.21.2", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/object.values": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", + "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "dev": true, + "dependencies": { + "default-browser": "^4.0.0", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pg": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.1.tgz", + "integrity": "sha512-utdq2obft07MxaDg0zBJI+l/M3mBRfIpEN3iSemsz0G5F2/VXx+XzqF4oxrbIZXQxt2AZzIUzyVg/YM6xOP/WQ==", + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.1", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.1.tgz", + "integrity": "sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg==" + }, + "node_modules/pg-format": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pg-format/-/pg-format-1.0.4.tgz", + "integrity": "sha512-YyKEF78pEA6wwTAqOUaHIN/rWpfzzIuMh9KdAhc3rSLQ/7zkRFcCgYBAEGatDstLyZw4g0s9SNICmaTGnBVeyw==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-range": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.3.tgz", + "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", + "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.6.2.tgz", + "integrity": "sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.0", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + }, + "node_modules/pure-rand": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.2.tgz", + "integrity": "sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/redis": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.7.tgz", + "integrity": "sha512-KrkuNJNpCwRm5vFJh0tteMxW8SaUzkm5fBH7eL5hd/D0fAkzvapxbfGPP/r+4JAXdQuX7nebsBkBqA2RHB7Usw==", + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.5.8", + "@redis/graph": "1.1.0", + "@redis/json": "1.0.4", + "@redis/search": "1.1.3", + "@redis/time-series": "1.0.4" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.3.tgz", + "integrity": "sha512-P8ur/gp/AmbEzjr729bZnLjXK5Z+4P0zhIJgBgzqRih7hL7BOukHGtSTA3ACMY467GRFz3duQsi0bDZdR7DKdw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.12.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-applescript": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", + "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/run-applescript/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/run-applescript/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/run-applescript/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/run-applescript/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", + "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", + "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dev": true, + "dependencies": { + "@pkgr/utils": "^2.3.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/synckit/node_modules/tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==", + "dev": true + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/titleize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", + "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/ts-jest": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz", + "integrity": "sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==", + "dev": true, + "dependencies": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/v8-to-istanbul": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/vm2": { + "version": "3.9.19", + "resolved": "https://registry.npmjs.org/vm2/-/vm2-3.9.19.tgz", + "integrity": "sha512-J637XF0DHDMV57R6JyVsTak7nIL8gy5KH4r1HiwWLf/4GBbb5MKL5y7LpmF4A8E2nR6XmzpmMFQ7V7ppPTmUQg==", + "dependencies": { + "acorn": "^8.7.0", + "acorn-walk": "^8.2.0" + }, + "bin": { + "vm2": "bin/vm2" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", + "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/runner/package.json b/runner/package.json new file mode 100644 index 000000000..0cfb5d41b --- /dev/null +++ b/runner/package.json @@ -0,0 +1,50 @@ +{ + "name": "redis-handler", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "rm -rf ./dist && tsc", + "start": "npm run build && node dist/index.js", + "start:dev": "ts-node ./src/index.ts", + "test": "node --experimental-vm-modules ./node_modules/.bin/jest", + "lint": "eslint -c .eslintrc.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@types/jest": "^29.5.3", + "@types/node": "^20.3.1", + "@types/node-fetch": "^2.6.4", + "@types/pg": "^8.10.2", + "@types/pg-format": "^1.0.2", + "@types/pluralize": "^0.0.29", + "@types/verror": "^1.10.6", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "eslint": "^8.46.0", + "eslint-config-prettier": "^8.9.0", + "eslint-config-standard-with-typescript": "^37.0.0", + "eslint-plugin-import": "^2.28.0", + "eslint-plugin-n": "^16.0.1", + "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-promise": "^6.1.1", + "jest": "^29.6.2", + "pluralize": "^8.0.0", + "prettier": "^3.0.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "^5.1.6" + }, + "dependencies": { + "@near-lake/primitives": "^0.1.0", + "aws-sdk": "^2.1402.0", + "node-fetch": "^2.6.11", + "pg": "^8.11.1", + "pg-format": "^1.0.4", + "redis": "^4.6.7", + "verror": "^1.10.1", + "vm2": "^3.9.19" + } +} diff --git a/runner/src/globals.d.ts b/runner/src/globals.d.ts new file mode 100644 index 000000000..d5ede3373 --- /dev/null +++ b/runner/src/globals.d.ts @@ -0,0 +1,12 @@ +declare namespace NodeJS { + export interface ProcessEnv { + HASURA_ENDPOINT: string + HASURA_ADMIN_SECRET: string + PG_HOST: string + PG_PORT: string + PG_ADMIN_USER: string + PG_ADMIN_PASSWORD: string + PG_ADMIN_DATABASE: string + REGION: string + } +} diff --git a/runner/src/hasura-client/__snapshots__/hasura-client.test.ts.snap b/runner/src/hasura-client/__snapshots__/hasura-client.test.ts.snap new file mode 100644 index 000000000..231391337 --- /dev/null +++ b/runner/src/hasura-client/__snapshots__/hasura-client.test.ts.snap @@ -0,0 +1,406 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`HasuraClient adds a datasource 1`] = ` +{ + "args": { + "configuration": { + "connection_info": { + "database_url": { + "connection_parameters": { + "database": "morgs_near", + "host": "localhost", + "password": "password", + "port": 5432, + "username": "morgs_near", + }, + }, + }, + }, + "name": "morgs_near", + }, + "type": "pg_add_source", +} +`; + +exports[`HasuraClient adds the specified permissions for the specified roles/table/schema 1`] = ` +{ + "args": [ + { + "args": { + "permission": { + "allow_aggregations": true, + "check": {}, + "columns": "*", + "computed_fields": [], + "filter": {}, + }, + "role": "role", + "source": "default", + "table": { + "name": "height", + "schema": "schema", + }, + }, + "type": "pg_create_select_permission", + }, + { + "args": { + "permission": { + "check": {}, + "columns": "*", + "computed_fields": [], + "filter": {}, + }, + "role": "role", + "source": "default", + "table": { + "name": "height", + "schema": "schema", + }, + }, + "type": "pg_create_insert_permission", + }, + { + "args": { + "permission": { + "check": {}, + "columns": "*", + "computed_fields": [], + "filter": {}, + }, + "role": "role", + "source": "default", + "table": { + "name": "height", + "schema": "schema", + }, + }, + "type": "pg_create_update_permission", + }, + { + "args": { + "permission": { + "check": {}, + "columns": "*", + "computed_fields": [], + "filter": {}, + }, + "role": "role", + "source": "default", + "table": { + "name": "height", + "schema": "schema", + }, + }, + "type": "pg_create_delete_permission", + }, + { + "args": { + "permission": { + "allow_aggregations": true, + "check": {}, + "columns": "*", + "computed_fields": [], + "filter": {}, + }, + "role": "role", + "source": "default", + "table": { + "name": "width", + "schema": "schema", + }, + }, + "type": "pg_create_select_permission", + }, + { + "args": { + "permission": { + "check": {}, + "columns": "*", + "computed_fields": [], + "filter": {}, + }, + "role": "role", + "source": "default", + "table": { + "name": "width", + "schema": "schema", + }, + }, + "type": "pg_create_insert_permission", + }, + { + "args": { + "permission": { + "check": {}, + "columns": "*", + "computed_fields": [], + "filter": {}, + }, + "role": "role", + "source": "default", + "table": { + "name": "width", + "schema": "schema", + }, + }, + "type": "pg_create_update_permission", + }, + { + "args": { + "permission": { + "check": {}, + "columns": "*", + "computed_fields": [], + "filter": {}, + }, + "role": "role", + "source": "default", + "table": { + "name": "width", + "schema": "schema", + }, + }, + "type": "pg_create_delete_permission", + }, + ], + "type": "bulk", +} +`; + +exports[`HasuraClient checks if a schema exists within source 1`] = ` +[ + [ + "mock-hasura-endpoint/v2/query", + { + "body": "{"type":"run_sql","args":{"sql":"SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'schema'","read_only":true,"source":"source"}}", + "headers": { + "X-Hasura-Admin-Secret": "mock-hasura-admin-secret", + }, + "method": "POST", + }, + ], +] +`; + +exports[`HasuraClient checks if datasource exists 1`] = ` +{ + "args": {}, + "type": "export_metadata", + "version": 2, +} +`; + +exports[`HasuraClient creates a schema 1`] = ` +[ + [ + "mock-hasura-endpoint/v2/query", + { + "body": "{"type":"run_sql","args":{"sql":"CREATE schema schemaName","read_only":false,"source":"dbName"}}", + "headers": { + "X-Hasura-Admin-Secret": "mock-hasura-admin-secret", + }, + "method": "POST", + }, + ], +] +`; + +exports[`HasuraClient gets table names within a schema 1`] = ` +{ + "args": { + "source": "source", + }, + "type": "pg_get_source_tables", +} +`; + +exports[`HasuraClient runs migrations for the specified schema 1`] = ` +[ + [ + "mock-hasura-endpoint/v2/query", + { + "body": "{"type":"run_sql","args":{"sql":"\\n set schema 'schemaName';\\n CREATE TABLE blocks (height numeric)\\n ","read_only":false,"source":"dbName"}}", + "headers": { + "X-Hasura-Admin-Secret": "mock-hasura-admin-secret", + }, + "method": "POST", + }, + ], +] +`; + +exports[`HasuraClient tracks foreign key relationships 1`] = ` +{ + "args": { + "read_only": true, + "source": "source", + "sql": " + SELECT + COALESCE(json_agg(row_to_json(info)), '[]'::JSON) + FROM ( + SELECT + q.table_schema::text AS table_schema, + q.table_name::text AS table_name, + q.constraint_name::text AS constraint_name, + min(q.ref_table_table_schema::text) AS ref_table_table_schema, + min(q.ref_table::text) AS ref_table, + json_object_agg(ac.attname, afc.attname) AS column_mapping, + min(q.confupdtype::text) AS on_update, + min(q.confdeltype::text) AS + on_delete + FROM ( + SELECT + ctn.nspname AS table_schema, + ct.relname AS table_name, + r.conrelid AS table_id, + r.conname AS constraint_name, + cftn.nspname AS ref_table_table_schema, + cft.relname AS ref_table, + r.confrelid AS ref_table_id, + r.confupdtype, + r.confdeltype, + unnest(r.conkey) AS column_id, + unnest(r.confkey) AS ref_column_id + FROM + pg_constraint r + JOIN pg_class ct ON r.conrelid = ct.oid + JOIN pg_namespace ctn ON ct.relnamespace = ctn.oid + JOIN pg_class cft ON r.confrelid = cft.oid + JOIN pg_namespace cftn ON cft.relnamespace = cftn.oid + WHERE + r.contype = 'f'::"char" + AND ((ctn.nspname='public')) + ) q + JOIN pg_attribute ac ON q.column_id = ac.attnum + AND q.table_id = ac.attrelid + JOIN pg_attribute afc ON q.ref_column_id = afc.attnum + AND q.ref_table_id = afc.attrelid + GROUP BY + q.table_schema, + q.table_name, + q.constraint_name) AS info; + ", + }, + "type": "run_sql", +} +`; + +exports[`HasuraClient tracks foreign key relationships 2`] = ` +{ + "args": [ + { + "args": { + "name": "comments", + "source": "source", + "table": { + "name": "posts", + "schema": "public", + }, + "using": { + "foreign_key_constraint_on": { + "column": "post_id", + "table": { + "name": "comments", + "schema": "public", + }, + }, + }, + }, + "type": "pg_create_array_relationship", + }, + { + "args": { + "name": "post", + "source": "source", + "table": { + "name": "comments", + "schema": "public", + }, + "using": { + "foreign_key_constraint_on": "post_id", + }, + }, + "type": "pg_create_object_relationship", + }, + { + "args": { + "name": "post_likes", + "source": "source", + "table": { + "name": "posts", + "schema": "public", + }, + "using": { + "foreign_key_constraint_on": { + "column": "post_id", + "table": { + "name": "post_likes", + "schema": "public", + }, + }, + }, + }, + "type": "pg_create_array_relationship", + }, + { + "args": { + "name": "post", + "source": "source", + "table": { + "name": "post_likes", + "schema": "public", + }, + "using": { + "foreign_key_constraint_on": "post_id", + }, + }, + "type": "pg_create_object_relationship", + }, + ], + "type": "bulk", +} +`; + +exports[`HasuraClient tracks the specified tables for a specified schema 1`] = ` +{ + "args": [ + { + "args": { + "source": "source", + "table": { + "name": "height", + "schema": "schema", + }, + }, + "type": "pg_track_table", + }, + { + "args": { + "source": "source", + "table": { + "name": "width", + "schema": "schema", + }, + }, + "type": "pg_track_table", + }, + ], + "type": "bulk", +} +`; + +exports[`HasuraClient untracks the specified tables 1`] = ` +[ + [ + "mock-hasura-endpoint/v1/metadata", + { + "body": "{"type":"bulk","args":[{"type":"pg_untrack_table","args":{"table":{"schema":"schema","name":"height"},"source":"default","cascade":true}},{"type":"pg_untrack_table","args":{"table":{"schema":"schema","name":"width"},"source":"default","cascade":true}}]}", + "headers": { + "X-Hasura-Admin-Secret": "mock-hasura-admin-secret", + }, + "method": "POST", + }, + ], +] +`; diff --git a/runner/src/hasura-client/hasura-client.test.ts b/runner/src/hasura-client/hasura-client.test.ts new file mode 100644 index 000000000..ef819374e --- /dev/null +++ b/runner/src/hasura-client/hasura-client.test.ts @@ -0,0 +1,247 @@ +import type fetch from 'node-fetch'; + +import HasuraClient from './hasura-client'; + +describe('HasuraClient', () => { + const oldEnv = process.env; + + const HASURA_ENDPOINT = 'mock-hasura-endpoint'; + const HASURA_ADMIN_SECRET = 'mock-hasura-admin-secret'; + const PG_HOST = 'localhost'; + const PG_PORT = '5432'; + + beforeAll(() => { + process.env = { + ...oldEnv, + HASURA_ENDPOINT, + HASURA_ADMIN_SECRET, + PG_HOST, + PG_PORT, + }; + }); + + afterAll(() => { + process.env = oldEnv; + }); + + it('creates a schema', async () => { + const mockFetch = jest + .fn() + .mockResolvedValue({ + status: 200, + text: () => JSON.stringify({}) + }); + const client = new HasuraClient({ fetch: mockFetch as unknown as typeof fetch }); + + await client.createSchema('dbName', 'schemaName'); + + expect(mockFetch.mock.calls).toMatchSnapshot(); + }); + + it('checks if a schema exists within source', async () => { + const mockFetch = jest + .fn() + .mockResolvedValue({ + status: 200, + text: () => JSON.stringify({ + result: [['schema_name'], ['name']] + }) + }); + const client = new HasuraClient({ fetch: mockFetch as unknown as typeof fetch }); + + const result = await client.doesSchemaExist('source', 'schema'); + + expect(result).toBe(true); + expect(mockFetch.mock.calls).toMatchSnapshot(); + }); + + it('checks if datasource exists', async () => { + const mockFetch = jest + .fn() + .mockResolvedValue({ + status: 200, + text: () => JSON.stringify({ + metadata: { + sources: [ + { + name: 'name' + } + ] + }, + }), + }); + const client = new HasuraClient({ fetch: mockFetch as unknown as typeof fetch }); + + await expect(client.doesSourceExist('name')).resolves.toBe(true); + expect(mockFetch.mock.calls[0][1].headers['X-Hasura-Admin-Secret']).toBe(HASURA_ADMIN_SECRET); + expect(JSON.parse(mockFetch.mock.calls[0][1].body)).toMatchSnapshot(); + }); + + it('runs migrations for the specified schema', async () => { + const mockFetch = jest + .fn() + .mockResolvedValue({ + status: 200, + text: () => JSON.stringify({}) + }); + const client = new HasuraClient({ fetch: mockFetch as unknown as typeof fetch }); + + await client.runMigrations('dbName', 'schemaName', 'CREATE TABLE blocks (height numeric)'); + + expect(mockFetch.mock.calls).toMatchSnapshot(); + }); + + it('gets table names within a schema', async () => { + const mockFetch = jest + .fn() + .mockResolvedValue({ + status: 200, + text: () => JSON.stringify([ + { name: 'table_name', schema: 'morgs_near' }, + { name: 'height', schema: 'schema' }, + { name: 'width', schema: 'schema' } + ]) + }); + const client = new HasuraClient({ fetch: mockFetch as unknown as typeof fetch }); + + const names = await client.getTableNames('schema', 'source'); + + expect(names).toEqual(['height', 'width']); + expect(mockFetch.mock.calls[0][1].headers['X-Hasura-Admin-Secret']).toBe(HASURA_ADMIN_SECRET); + expect(JSON.parse(mockFetch.mock.calls[0][1].body)).toMatchSnapshot(); + }); + + it('tracks the specified tables for a specified schema', async () => { + const mockFetch = jest + .fn() + .mockResolvedValue({ + status: 200, + text: () => JSON.stringify({}) + }); + const client = new HasuraClient({ fetch: mockFetch as unknown as typeof fetch }); + + await client.trackTables('schema', ['height', 'width'], 'source'); + + expect(mockFetch.mock.calls[0][1].headers['X-Hasura-Admin-Secret']).toBe(HASURA_ADMIN_SECRET); + expect(JSON.parse(mockFetch.mock.calls[0][1].body)).toMatchSnapshot(); + }); + + it('untracks the specified tables', async () => { + const mockFetch = jest + .fn() + .mockResolvedValue({ + status: 200, + text: () => JSON.stringify({}) + }); + const client = new HasuraClient({ fetch: mockFetch as unknown as typeof fetch }); + + await client.untrackTables('default', 'schema', ['height', 'width']); + + expect(mockFetch.mock.calls).toMatchSnapshot(); + }); + + it('adds the specified permissions for the specified roles/table/schema', async () => { + const mockFetch = jest + .fn() + .mockResolvedValue({ + status: 200, + text: () => JSON.stringify({}) + }); + const client = new HasuraClient({ fetch: mockFetch as unknown as typeof fetch }); + + await client.addPermissionsToTables('schema', 'default', ['height', 'width'], 'role', ['select', 'insert', 'update', 'delete']); + + expect(mockFetch.mock.calls[0][1].headers['X-Hasura-Admin-Secret']).toBe(HASURA_ADMIN_SECRET); + expect(JSON.parse(mockFetch.mock.calls[0][1].body)).toMatchSnapshot(); + }); + + it('adds a datasource', async () => { + const mockFetch = jest + .fn() + .mockResolvedValue({ + status: 200, + text: () => JSON.stringify({}) + }); + const client = new HasuraClient({ fetch: mockFetch as unknown as typeof fetch }); + + await client.addDatasource('morgs_near', 'password', 'morgs_near'); + + expect(mockFetch.mock.calls[0][1].headers['X-Hasura-Admin-Secret']).toBe(HASURA_ADMIN_SECRET); + expect(JSON.parse(mockFetch.mock.calls[0][1].body)).toMatchSnapshot(); + }); + + it('tracks foreign key relationships', async () => { + const mockFetch = jest + .fn() + .mockResolvedValue({ + status: 200, + text: () => JSON.stringify({ + result: [ + [ + 'coalesce' + ], + [ + JSON.stringify([ + { + table_schema: 'public', + table_name: 'comments', + constraint_name: 'comments_post_id_fkey', + ref_table_table_schema: 'public', + ref_table: 'posts', + column_mapping: { + post_id: 'id' + }, + on_update: 'a', + on_delete: 'a' + }, + { + table_schema: 'public', + table_name: 'post_likes', + constraint_name: 'post_likes_post_id_fkey', + ref_table_table_schema: 'public', + ref_table: 'posts', + column_mapping: { + post_id: 'id' + }, + on_update: 'a', + on_delete: 'c' + } + ]) + ] + ] + }), + }); + const client = new HasuraClient({ fetch: mockFetch as unknown as typeof fetch }); + + await client.trackForeignKeyRelationships('public', 'source'); + + expect(mockFetch.mock.calls[0][1].headers['X-Hasura-Admin-Secret']).toBe(HASURA_ADMIN_SECRET); + expect(JSON.parse(mockFetch.mock.calls[0][1].body)).toMatchSnapshot(); + + expect(mockFetch.mock.calls[1][1].headers['X-Hasura-Admin-Secret']).toBe(HASURA_ADMIN_SECRET); + expect(JSON.parse(mockFetch.mock.calls[1][1].body)).toMatchSnapshot(); + }); + + it('skips foreign key tracking if none exist', async () => { + const mockFetch = jest + .fn() + .mockResolvedValue({ + status: 200, + text: () => JSON.stringify({ + result: [ + [ + 'coalesce' + ], + [ + JSON.stringify([]) + ] + ] + }), + }); + const client = new HasuraClient({ fetch: mockFetch as unknown as typeof fetch }); + + await client.trackForeignKeyRelationships('public', 'source'); + + expect(mockFetch).toBeCalledTimes(1); // to fetch the foreign keys + }); +}); diff --git a/runner/src/hasura-client/hasura-client.ts b/runner/src/hasura-client/hasura-client.ts new file mode 100644 index 000000000..c7aa7ea58 --- /dev/null +++ b/runner/src/hasura-client/hasura-client.ts @@ -0,0 +1,339 @@ +import fetch, { type Response } from 'node-fetch'; +import pluralize from 'pluralize'; + +interface Dependencies { + fetch: typeof fetch +} + +interface SqlOptions { + readOnly: boolean + source?: string +} + +type MetadataRequestArgs = Record; + +type MetadataRequests = Record; + +export default class HasuraClient { + static DEFAULT_DATABASE = 'default'; + static DEFAULT_SCHEMA = 'public'; + + private readonly deps: Dependencies; + + constructor (deps?: Partial) { + this.deps = { + fetch, + ...deps, + }; + } + + async executeSql (sql: string, opts: SqlOptions): Promise { + const response: Response = await this.deps.fetch( + `${process.env.HASURA_ENDPOINT}/v2/query`, + { + method: 'POST', + headers: { + 'X-Hasura-Admin-Secret': process.env.HASURA_ADMIN_SECRET, + }, + body: JSON.stringify({ + type: 'run_sql', + args: { + sql, + read_only: opts.readOnly, + source: opts.source ?? 'default', + }, + }), + } + ); + + const body: string = await response.text(); + + if (response.status !== 200) { + throw new Error(body); + } + + return JSON.parse(body); + } + + async executeMetadataRequest ( + type: string, + args: MetadataRequestArgs, + version?: number + ): Promise { + const response: Response = await this.deps.fetch( + `${process.env.HASURA_ENDPOINT}/v1/metadata`, + { + method: 'POST', + headers: { + 'X-Hasura-Admin-Secret': process.env.HASURA_ADMIN_SECRET, + }, + body: JSON.stringify({ + type, + args, + ...(version && { version }), + }), + } + ); + + const body: string = await response.text(); + + if (response.status !== 200) { + throw new Error(body); + } + + return JSON.parse(body); + } + + async executeBulkMetadataRequest ( + metadataRequests: MetadataRequests + ): Promise { + return await this.executeMetadataRequest('bulk', metadataRequests); + } + + async exportMetadata (): Promise { + const { metadata } = await this.executeMetadataRequest( + 'export_metadata', + {}, + 2 + ); + return metadata; + } + + async doesSourceExist (source: string): Promise { + const metadata = await this.exportMetadata(); + return metadata.sources.filter(({ name }: { name: string }) => name === source).length > 0; + } + + async doesSchemaExist (source: string, schemaName: string): Promise { + const { result } = await this.executeSql( + `SELECT schema_name FROM information_schema.schemata WHERE schema_name = '${schemaName}'`, + { source, readOnly: true } + ); + + return result.length > 1; + } + + async createSchema (source: string, schemaName: string): Promise { + return await this.executeSql(`CREATE schema ${schemaName}`, { + source, + readOnly: false, + }); + } + + async runMigrations (source: string, schemaName: string, migration: string): Promise { + return await this.executeSql( + ` + set schema '${schemaName}'; + ${migration} + `, + { source, readOnly: false } + ); + } + + async getTableNames (schemaName: string, source: string): Promise { + const tablesInSource = await this.executeMetadataRequest( + 'pg_get_source_tables', + { + source, + } + ); + + return tablesInSource + .filter(({ schema }: { schema: string }) => schema === schemaName) + .map(({ name }: { name: string }) => name); + } + + async trackTables ( + schemaName: string, + tableNames: string[], + source: string + ): Promise { + return await this.executeBulkMetadataRequest( + tableNames.map(name => ({ + type: 'pg_track_table', + args: { + source, + table: { + name, + schema: schemaName, + }, + }, + })) + ); + } + + async untrackTables ( + source: string, + schema: string, + tableNames: string[], + cascade = true + ): Promise { + return await this.executeBulkMetadataRequest( + tableNames.map(name => ({ + type: 'pg_untrack_table', + args: { + table: { + schema, + name, + }, + source, + cascade, + }, + })) + ); + } + + async getForeignKeys (schemaName: string, source: string): Promise { + const { result } = await this.executeSql( + ` + SELECT + COALESCE(json_agg(row_to_json(info)), '[]'::JSON) + FROM ( + SELECT + q.table_schema::text AS table_schema, + q.table_name::text AS table_name, + q.constraint_name::text AS constraint_name, + min(q.ref_table_table_schema::text) AS ref_table_table_schema, + min(q.ref_table::text) AS ref_table, + json_object_agg(ac.attname, afc.attname) AS column_mapping, + min(q.confupdtype::text) AS on_update, + min(q.confdeltype::text) AS + on_delete + FROM ( + SELECT + ctn.nspname AS table_schema, + ct.relname AS table_name, + r.conrelid AS table_id, + r.conname AS constraint_name, + cftn.nspname AS ref_table_table_schema, + cft.relname AS ref_table, + r.confrelid AS ref_table_id, + r.confupdtype, + r.confdeltype, + unnest(r.conkey) AS column_id, + unnest(r.confkey) AS ref_column_id + FROM + pg_constraint r + JOIN pg_class ct ON r.conrelid = ct.oid + JOIN pg_namespace ctn ON ct.relnamespace = ctn.oid + JOIN pg_class cft ON r.confrelid = cft.oid + JOIN pg_namespace cftn ON cft.relnamespace = cftn.oid + WHERE + r.contype = 'f'::"char" + AND ((ctn.nspname='${schemaName}')) + ) q + JOIN pg_attribute ac ON q.column_id = ac.attnum + AND q.table_id = ac.attrelid + JOIN pg_attribute afc ON q.ref_column_id = afc.attnum + AND q.ref_table_id = afc.attrelid + GROUP BY + q.table_schema, + q.table_name, + q.constraint_name) AS info; + `, + { readOnly: true, source } + ); + + const [, [foreignKeysJsonString]] = result; + + return JSON.parse(foreignKeysJsonString); + } + + async trackForeignKeyRelationships ( + schemaName: string, + source: string + ): Promise { + const foreignKeys = await this.getForeignKeys(schemaName, source); + + if (foreignKeys.length === 0) { + return; + } + + return await this.executeBulkMetadataRequest( + foreignKeys + .map((foreignKey) => ([ + { + type: 'pg_create_array_relationship', + args: { + source, + name: foreignKey.table_name, + table: { + name: foreignKey.ref_table, + schema: schemaName, + }, + using: { + foreign_key_constraint_on: { + table: { + name: foreignKey.table_name, + schema: schemaName, + }, + column: Object.keys(foreignKey.column_mapping)[0], + } + }, + } + }, + { + type: 'pg_create_object_relationship', + args: { + source, + name: pluralize.singular(foreignKey.ref_table), + table: { + name: foreignKey.table_name, + schema: schemaName, + }, + using: { + foreign_key_constraint_on: Object.keys(foreignKey.column_mapping)[0], + }, + } + }, + ])) + .flat() + ); + } + + async addPermissionsToTables (schemaName: string, source: string, tableNames: string[], roleName: string, permissions: string[]): Promise { + return await this.executeBulkMetadataRequest( + tableNames + .map((tableName) => ( + permissions.map((permission) => ({ + type: `pg_create_${permission}_permission`, + args: { + source, + table: { + name: tableName, + schema: schemaName, + }, + role: roleName, + permission: { + columns: '*', + check: {}, + computed_fields: [], + filter: {}, + ...(permission === 'select' && { allow_aggregations: true }) + }, + }, + })) + )) + .flat() + ); + } + + async addDatasource (userName: string, password: string, databaseName: string): Promise { + return await this.executeMetadataRequest('pg_add_source', { + name: databaseName, + configuration: { + connection_info: { + database_url: { + connection_parameters: { + password, + database: databaseName, + username: userName, + host: process.env.PG_HOST, + port: Number(process.env.PG_PORT), + } + }, + }, + }, + }); + } +} diff --git a/runner/src/hasura-client/index.ts b/runner/src/hasura-client/index.ts new file mode 100644 index 000000000..be6fcaaec --- /dev/null +++ b/runner/src/hasura-client/index.ts @@ -0,0 +1 @@ +export { default } from './hasura-client'; diff --git a/runner/src/index.ts b/runner/src/index.ts new file mode 100644 index 000000000..d3bbe336b --- /dev/null +++ b/runner/src/index.ts @@ -0,0 +1,156 @@ +import { createClient } from 'redis'; + +import Indexer from './indexer'; + +const client = createClient({ url: process.env.REDIS_CONNECTION_STRING }); +const indexer = new Indexer('mainnet'); + +// const BATCH_SIZE = 1; +const STREAM_START_ID = '0'; +// const STREAM_THROTTLE_MS = 250; +const STREAM_HANDLER_THROTTLE_MS = 500; + +const INDEXER_SET_KEY = 'indexers'; + +client.on('error', (err) => { console.log('Redis Client Error', err); }); + +const generateStreamKey = (name: string): string => { + return `${name}:stream`; +}; + +const generateStorageKey = (name: string): string => { + return `${name}:storage`; +}; + +const generateStreamLastIdKey = (name: string): string => { + return `${name}:stream:lastId`; +}; + +const runFunction = async (indexerName: string, blockHeight: string): Promise => { + const { account_id: accountId, function_name: functionName, code, schema } = await getIndexerData( + indexerName, + ); + + const functions = { + [indexerName]: { + account_id: accountId, + function_name: functionName, + code, + schema, + provisioned: false, + }, + }; + + await indexer.runFunctions(Number(blockHeight), functions, false, { + provision: true, + }); +}; + +type StreamMessages = Array<{ + id: string + message: Message +}>; + +const getMessagesFromStream = async >( + indexerName: string, + lastId: string | null, + count: number, +): Promise | null> => { + const id = lastId ?? STREAM_START_ID; + + const results = await client.xRead( + { key: generateStreamKey(indexerName), id }, + // can't use blocking calls as running single threaded + { COUNT: count } + ); + + return results?.[0].messages as StreamMessages; +}; + +const getLastProcessedId = async ( + indexerName: string, +): Promise => { + return await client.get(generateStreamLastIdKey(indexerName)); +}; + +const setLastProcessedId = async ( + indexerName: string, + lastId: string, +): Promise => { + await client.set(generateStreamLastIdKey(indexerName), lastId); +}; + +interface IndexerConfig { + account_id: string + function_name: string + code: string + schema: string +} + +const getIndexerData = async (indexerName: string): Promise => { + const results = await client.get(generateStorageKey(indexerName)); + + if (results === null) { + throw new Error(`${indexerName} does not have any data`); + } + + return JSON.parse(results); +}; + +type IndexerStreamMessage = Record; + +const processStream = async (indexerName: string): Promise => { + while (true) { + try { + const lastProcessedId = await getLastProcessedId(indexerName); + const messages = await getMessagesFromStream( + indexerName, + lastProcessedId, + 1, + ); + + if (messages == null) { + continue; + } + + const [{ id, message }] = messages; + + await runFunction(indexerName, message.block_height); + + await setLastProcessedId(indexerName, id); + + console.log(`Success: ${indexerName}`); + } catch (err) { + console.log(`Failed: ${indexerName}`, err); + } + } +}; + +type StreamHandlers = Record>; + +void (async function main () { + try { + await client.connect(); + + const streamHandlers: StreamHandlers = {}; + + while (true) { + const indexers = await client.sMembers(INDEXER_SET_KEY); + + indexers.forEach((indexerName) => { + if (streamHandlers[indexerName] !== undefined) { + return; + } + + const handler = processStream(indexerName); + streamHandlers[indexerName] = handler; + }); + + await new Promise((resolve) => + setTimeout(resolve, STREAM_HANDLER_THROTTLE_MS), + ); + } + } finally { + await client.disconnect(); + } +})(); diff --git a/runner/src/indexer/__snapshots__/indexer.test.ts.snap b/runner/src/indexer/__snapshots__/indexer.test.ts.snap new file mode 100644 index 000000000..691dcf902 --- /dev/null +++ b/runner/src/indexer/__snapshots__/indexer.test.ts.snap @@ -0,0 +1,327 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Indexer unit tests Indexer.buildContext() can fetch from the near social api 1`] = ` +[ + [ + "https://api.near.social/index", + { + "body": "{"action":"post","key":"main","options":{"limit":1,"order":"desc"}}", + "headers": { + "Content-Type": "application/json", + }, + "method": "POST", + }, + ], +] +`; + +exports[`Indexer unit tests Indexer.runFunctions() allows imperative execution of GraphQL operations 1`] = ` +[ + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"mutation writeLog($function_name: String!, $block_height: numeric!, $message: String!){\\n insert_indexer_log_entries_one(object: {function_name: $function_name, block_height: $block_height, message: $message}) {id}\\n }","variables":{"function_name":"buildnear.testnet/test","block_height":82699904,"message":"Running function buildnear.testnet/test, lag is: NaNms from block timestamp"}}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "append", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"\\n mutation SetStatus($function_name: String, $status: String) {\\n insert_indexer_state_one(object: {function_name: $function_name, status: $status, current_block_height: 0 }, on_conflict: { constraint: indexer_state_pkey, update_columns: status }) {\\n function_name\\n status\\n }\\n }\\n ","variables":{"function_name":"buildnear.testnet/test","status":"RUNNING"}}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "append", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"\\n query {\\n posts(where: { id: { _eq: 1 } }) {\\n id\\n }\\n }\\n "}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "buildnear_testnet", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"\\n mutation {\\n insert_comments(\\n objects: {account_id: \\"morgs.near\\", block_height: 82699904, content: \\"cool post\\", post_id: 1}\\n ) {\\n returning {\\n id\\n }\\n }\\n }\\n "}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "buildnear_testnet", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"mutation WriteBlock($function_name: String!, $block_height: numeric!) {\\n insert_indexer_state(\\n objects: {current_block_height: $block_height, function_name: $function_name}\\n on_conflict: {constraint: indexer_state_pkey, update_columns: current_block_height}\\n ) {\\n returning {\\n current_block_height\\n function_name\\n }\\n }\\n }","variables":{"function_name":"buildnear.testnet/test","block_height":82699904}}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "append", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], +] +`; + +exports[`Indexer unit tests Indexer.runFunctions() catches errors 1`] = ` +[ + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"mutation writeLog($function_name: String!, $block_height: numeric!, $message: String!){\\n insert_indexer_log_entries_one(object: {function_name: $function_name, block_height: $block_height, message: $message}) {id}\\n }","variables":{"function_name":"buildnear.testnet/test","block_height":456,"message":"Running function buildnear.testnet/test, lag is: NaNms from block timestamp"}}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "append", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"\\n mutation SetStatus($function_name: String, $status: String) {\\n insert_indexer_state_one(object: {function_name: $function_name, status: $status, current_block_height: 0 }, on_conflict: { constraint: indexer_state_pkey, update_columns: status }) {\\n function_name\\n status\\n }\\n }\\n ","variables":{"function_name":"buildnear.testnet/test","status":"RUNNING"}}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "append", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"mutation writeLog($function_name: String!, $block_height: numeric!, $message: String!){\\n insert_indexer_log_entries_one(object: {function_name: $function_name, block_height: $block_height, message: $message}) {id}\\n }","variables":{"function_name":"buildnear.testnet/test","block_height":456,"message":"Error running IndexerFunction:boom"}}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "append", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"\\n mutation SetStatus($function_name: String, $status: String) {\\n insert_indexer_state_one(object: {function_name: $function_name, status: $status, current_block_height: 0 }, on_conflict: { constraint: indexer_state_pkey, update_columns: status }) {\\n function_name\\n status\\n }\\n }\\n ","variables":{"function_name":"buildnear.testnet/test","status":"STOPPED"}}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "append", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], +] +`; + +exports[`Indexer unit tests Indexer.runFunctions() logs provisioning failures 1`] = ` +[ + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"mutation writeLog($function_name: String!, $block_height: numeric!, $message: String!){\\n insert_indexer_log_entries_one(object: {function_name: $function_name, block_height: $block_height, message: $message}) {id}\\n }","variables":{"function_name":"morgs.near/test","block_height":82699904,"message":"Running function morgs.near/test, lag is: NaNms from block timestamp"}}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "append", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"\\n mutation SetStatus($function_name: String, $status: String) {\\n insert_indexer_state_one(object: {function_name: $function_name, status: $status, current_block_height: 0 }, on_conflict: { constraint: indexer_state_pkey, update_columns: status }) {\\n function_name\\n status\\n }\\n }\\n ","variables":{"function_name":"morgs.near/test","status":"PROVISIONING"}}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "append", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"mutation writeLog($function_name: String!, $block_height: numeric!, $message: String!){\\n insert_indexer_log_entries_one(object: {function_name: $function_name, block_height: $block_height, message: $message}) {id}\\n }","variables":{"function_name":"morgs.near/test","block_height":82699904,"message":"Provisioning endpoint: starting"}}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "append", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"mutation writeLog($function_name: String!, $block_height: numeric!, $message: String!){\\n insert_indexer_log_entries_one(object: {function_name: $function_name, block_height: $block_height, message: $message}) {id}\\n }","variables":{"function_name":"morgs.near/test","block_height":82699904,"message":"Provisioning endpoint: failure:something went wrong with provisioning"}}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "append", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"\\n mutation SetStatus($function_name: String, $status: String) {\\n insert_indexer_state_one(object: {function_name: $function_name, status: $status, current_block_height: 0 }, on_conflict: { constraint: indexer_state_pkey, update_columns: status }) {\\n function_name\\n status\\n }\\n }\\n ","variables":{"function_name":"morgs.near/test","status":"STOPPED"}}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "append", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], +] +`; + +exports[`Indexer unit tests Indexer.runFunctions() should execute all functions against the current block 1`] = ` +[ + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"mutation writeLog($function_name: String!, $block_height: numeric!, $message: String!){\\n insert_indexer_log_entries_one(object: {function_name: $function_name, block_height: $block_height, message: $message}) {id}\\n }","variables":{"function_name":"buildnear.testnet/test","block_height":456,"message":"Running function buildnear.testnet/test, lag is: NaNms from block timestamp"}}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "append", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"\\n mutation SetStatus($function_name: String, $status: String) {\\n insert_indexer_state_one(object: {function_name: $function_name, status: $status, current_block_height: 0 }, on_conflict: { constraint: indexer_state_pkey, update_columns: status }) {\\n function_name\\n status\\n }\\n }\\n ","variables":{"function_name":"buildnear.testnet/test","status":"RUNNING"}}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "append", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"mutation { set(functionName: \\"buildnear.testnet/test\\", key: \\"height\\", data: \\"456\\")}"}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "buildnear_testnet", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"mutation WriteBlock($function_name: String!, $block_height: numeric!) {\\n insert_indexer_state(\\n objects: {current_block_height: $block_height, function_name: $function_name}\\n on_conflict: {constraint: indexer_state_pkey, update_columns: current_block_height}\\n ) {\\n returning {\\n current_block_height\\n function_name\\n }\\n }\\n }","variables":{"function_name":"buildnear.testnet/test","block_height":456}}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "append", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], +] +`; + +exports[`Indexer unit tests Indexer.runFunctions() supplies the required role to the GraphQL endpoint 1`] = ` +[ + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"mutation writeLog($function_name: String!, $block_height: numeric!, $message: String!){\\n insert_indexer_log_entries_one(object: {function_name: $function_name, block_height: $block_height, message: $message}) {id}\\n }","variables":{"function_name":"morgs.near/test","block_height":82699904,"message":"Running function morgs.near/test, lag is: NaNms from block timestamp"}}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "append", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"\\n mutation SetStatus($function_name: String, $status: String) {\\n insert_indexer_state_one(object: {function_name: $function_name, status: $status, current_block_height: 0 }, on_conflict: { constraint: indexer_state_pkey, update_columns: status }) {\\n function_name\\n status\\n }\\n }\\n ","variables":{"function_name":"morgs.near/test","status":"RUNNING"}}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "append", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"mutation { set(functionName: \\"buildnear.testnet/test\\", key: \\"height\\", data: \\"82699904\\")}"}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "morgs_near", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], + [ + "mock-hasura-endpoint/v1/graphql", + { + "body": "{"query":"mutation WriteBlock($function_name: String!, $block_height: numeric!) {\\n insert_indexer_state(\\n objects: {current_block_height: $block_height, function_name: $function_name}\\n on_conflict: {constraint: indexer_state_pkey, update_columns: current_block_height}\\n ) {\\n returning {\\n current_block_height\\n function_name\\n }\\n }\\n }","variables":{"function_name":"morgs.near/test","block_height":82699904}}", + "headers": { + "Content-Type": "application/json", + "X-Hasura-Admin-Secret": "mock-hasura-secret", + "X-Hasura-Role": "append", + "X-Hasura-Use-Backend-Only-Permissions": "true", + }, + "method": "POST", + }, + ], +] +`; diff --git a/runner/src/indexer/index.ts b/runner/src/indexer/index.ts new file mode 100644 index 000000000..2531410f8 --- /dev/null +++ b/runner/src/indexer/index.ts @@ -0,0 +1 @@ +export { default } from './indexer'; diff --git a/runner/src/indexer/indexer.test.ts b/runner/src/indexer/indexer.test.ts new file mode 100644 index 000000000..f83165d7d --- /dev/null +++ b/runner/src/indexer/indexer.test.ts @@ -0,0 +1,762 @@ +import { Block } from '@near-lake/primitives'; +import type fetch from 'node-fetch'; +import type AWS from 'aws-sdk'; + +import Indexer from './indexer'; +import { VM } from 'vm2'; + +describe('Indexer unit tests', () => { + const oldEnv = process.env; + + const HASURA_ENDPOINT = 'mock-hasura-endpoint'; + const HASURA_ADMIN_SECRET = 'mock-hasura-secret'; + + beforeAll(() => { + process.env = { + ...oldEnv, + HASURA_ENDPOINT, + HASURA_ADMIN_SECRET + }; + }); + + afterAll(() => { + process.env = oldEnv; + }); + + test('Indexer.runFunctions() should execute all functions against the current block', async () => { + const mockFetch = jest.fn(() => ({ + status: 200, + json: async () => ({ + errors: null, + }), + })); + const blockHeight = 456; + const mockS3 = { + getObject: jest.fn(() => ({ + promise: async () => await Promise.resolve({ + Body: { + toString: () => JSON.stringify({ + chunks: [], + header: { + height: blockHeight + } + }) + } + }) + })), + } as unknown as AWS.S3; + const indexer = new Indexer('mainnet', { fetch: mockFetch as unknown as typeof fetch, s3: mockS3 }); + + const functions: Record = {}; + functions['buildnear.testnet/test'] = { + code: ` + const foo = 3; + block.result = context.graphql(\`mutation { set(functionName: "buildnear.testnet/test", key: "height", data: "\${block.blockHeight}")}\`); + ` + }; + await indexer.runFunctions(blockHeight, functions, false); + + expect(mockFetch.mock.calls).toMatchSnapshot(); + }); + + test('Indexer.fetchBlock() should fetch a block from the S3', async () => { + const author = 'dokiacapital.poolv1.near'; + const mockS3 = { + getObject: jest.fn(() => ({ + promise: async () => await Promise.resolve({ + Body: { + toString: () => JSON.stringify({ + author + }) + } + }) + })), + } as unknown as AWS.S3; + const indexer = new Indexer('mainnet', { s3: mockS3 }); + + const blockHeight = 84333960; + const block = await indexer.fetchBlockPromise(blockHeight); + + expect(mockS3.getObject).toHaveBeenCalledTimes(1); + expect(mockS3.getObject).toHaveBeenCalledWith({ + Bucket: 'near-lake-data-mainnet', + Key: `${blockHeight.toString().padStart(12, '0')}/block.json` + }); + expect(block.author).toEqual(author); + }); + + test('Indexer.fetchShard() should fetch the steamer message from S3', async () => { + const mockS3 = { + getObject: jest.fn(() => ({ + promise: async () => await Promise.resolve({ + Body: { + toString: () => JSON.stringify({}) + } + }) + })), + } as unknown as AWS.S3; + const indexer = new Indexer('mainnet', { s3: mockS3 }); + + const blockHeight = 82699904; + const shard = 0; + await indexer.fetchShardPromise(blockHeight, shard); + + expect(mockS3.getObject).toHaveBeenCalledTimes(1); + expect(mockS3.getObject).toHaveBeenCalledWith({ + Bucket: 'near-lake-data-mainnet', + Key: `${blockHeight.toString().padStart(12, '0')}/shard_${shard}.json` + }); + }); + + test('Indexer.fetchStreamerMessage() should fetch the block/shards and construct the streamer message', async () => { + const blockHeight = 85233529; + const blockHash = 'xyz'; + const getObject = jest.fn() + .mockReturnValueOnce({ // block + promise: async () => await Promise.resolve({ + Body: { + toString: () => JSON.stringify({ + chunks: [0], + header: { + height: blockHeight, + hash: blockHash, + } + }) + } + }) + }) + .mockReturnValue({ // shard + promise: async () => await Promise.resolve({ + Body: { + toString: () => JSON.stringify({}) + } + }) + }); + const mockS3 = { + getObject, + } as unknown as AWS.S3; + const indexer = new Indexer('mainnet', { s3: mockS3 }); + + const streamerMessage = await indexer.fetchStreamerMessage(blockHeight); + + expect(getObject).toHaveBeenCalledTimes(5); + expect(getObject.mock.calls[0][0]).toEqual({ + Bucket: 'near-lake-data-mainnet', + Key: `${blockHeight.toString().padStart(12, '0')}/block.json` + }); + expect(getObject.mock.calls[1][0]).toEqual({ + Bucket: 'near-lake-data-mainnet', + Key: `${blockHeight.toString().padStart(12, '0')}/shard_0.json` + }); + + const block = Block.fromStreamerMessage(streamerMessage); + + expect(block.blockHeight).toEqual(blockHeight); + expect(block.blockHash).toEqual(blockHash); + }); + + test('Indexer.transformIndexerFunction() applies the necessary transformations', () => { + const indexer = new Indexer('mainnet'); + + const transformedFunction = indexer.transformIndexerFunction('console.log(\'hello\')'); + + expect(transformedFunction).toEqual(` + async function f(){ + console.log('hello') + }; + f(); + `); + }); + + test('Indexer.buildContext() allows execution of arbitrary GraphQL operations', async () => { + const mockFetch = jest.fn() + .mockResolvedValueOnce({ + status: 200, + json: async () => ({ + data: { + greet: 'hello' + } + }) + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => ({ + data: { + newGreeting: { + success: true + } + } + }) + }); + const indexer = new Indexer('mainnet', { fetch: mockFetch as unknown as typeof fetch }); + + const context = indexer.buildContext('test', 'morgs.near/test', 1, 'morgs_near'); + + const query = ` + query { + greet() + } + `; + const { greet } = await context.graphql(query) as { greet: string }; + + const mutation = ` + mutation { + newGreeting(greeting: "${greet} morgan") { + success + } + } + `; + const { newGreeting: { success } } = await context.graphql(mutation); + + expect(greet).toEqual('hello'); + expect(success).toEqual(true); + expect(mockFetch.mock.calls[0]).toEqual([ + `${HASURA_ENDPOINT}/v1/graphql`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hasura-Use-Backend-Only-Permissions': 'true', + 'X-Hasura-Role': 'morgs_near', + 'X-Hasura-Admin-Secret': HASURA_ADMIN_SECRET + }, + body: JSON.stringify({ query }) + } + ]); + expect(mockFetch.mock.calls[1]).toEqual([ + `${HASURA_ENDPOINT}/v1/graphql`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hasura-Use-Backend-Only-Permissions': 'true', + 'X-Hasura-Role': 'morgs_near', + 'X-Hasura-Admin-Secret': HASURA_ADMIN_SECRET + }, + body: JSON.stringify({ query: mutation }) + } + ]); + }); + + test('Indexer.buildContext() can fetch from the near social api', async () => { + const mockFetch = jest.fn(); + const indexer = new Indexer('mainnet', { fetch: mockFetch as unknown as typeof fetch }); + + const context = indexer.buildContext('test', 'morgs.near/test', 1, 'role'); + + await context.fetchFromSocialApi('/index', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + action: 'post', + key: 'main', + options: { + limit: 1, + order: 'desc' + } + }) + }); + + expect(mockFetch.mock.calls).toMatchSnapshot(); + }); + + test('Indexer.buildContext() throws when a GraphQL response contains errors', async () => { + const mockFetch = jest.fn() + .mockResolvedValue({ + json: async () => ({ + errors: ['boom'] + }) + }); + const indexer = new Indexer('mainnet', { fetch: mockFetch as unknown as typeof fetch }); + + const context = indexer.buildContext('test', 'morgs.near/test', 1, 'role'); + + await expect(async () => await context.graphql('query { hello }')).rejects.toThrow('boom'); + }); + + test('Indexer.buildContext() handles GraphQL variables', async () => { + const mockFetch = jest.fn() + .mockResolvedValue({ + status: 200, + json: async () => ({ + data: 'mock', + }), + }); + const indexer = new Indexer('mainnet', { fetch: mockFetch as unknown as typeof fetch }); + + const context = indexer.buildContext('test', 'morgs.near/test', 1, 'morgs_near'); + + const query = 'query($name: String) { hello(name: $name) }'; + const variables = { name: 'morgan' }; + await context.graphql(query, variables); + + expect(mockFetch.mock.calls[0]).toEqual([ + `${HASURA_ENDPOINT}/v1/graphql`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hasura-Use-Backend-Only-Permissions': 'true', + 'X-Hasura-Role': 'morgs_near', + 'X-Hasura-Admin-Secret': HASURA_ADMIN_SECRET + }, + body: JSON.stringify({ + query, + variables, + }), + }, + ]); + }); + + test('Indexer.runFunctions() allows imperative execution of GraphQL operations', async () => { + const postId = 1; + const commentId = 2; + const blockHeight = 82699904; + const mockFetch = jest.fn() + .mockReturnValueOnce({ // starting log + status: 200, + json: async () => ({ + data: { + indexer_log_store: [ + { + id: '12345', + }, + ], + }, + }), + }) + .mockReturnValueOnce({ + status: 200, + json: async () => ({ + errors: null, + }), + }) + .mockReturnValueOnce({ // query + status: 200, + json: async () => ({ + data: { + posts: [ + { + id: postId, + }, + ], + }, + }), + }) + .mockReturnValueOnce({ // mutation + status: 200, + json: async () => ({ + data: { + insert_comments: { + returning: { + id: commentId, + }, + }, + }, + }), + }) + .mockReturnValueOnce({ + status: 200, + json: async () => ({ + errors: null, + }), + }); + + const mockS3 = { + getObject: jest.fn() + .mockReturnValueOnce({ // block + promise: async () => await Promise.resolve({ + Body: { + toString: () => JSON.stringify({ + chunks: [0], + header: { + height: blockHeight, + }, + }), + }, + }), + }) + .mockReturnValue({ // shard + promise: async () => await Promise.resolve({ + Body: { + toString: () => JSON.stringify({}) + }, + }), + }), + } as unknown as AWS.S3; + const indexer = new Indexer('mainnet', { fetch: mockFetch as unknown as typeof fetch, s3: mockS3 }); + + const functions: Record = {}; + functions['buildnear.testnet/test'] = { + code: ` + const { posts } = await context.graphql(\` + query { + posts(where: { id: { _eq: 1 } }) { + id + } + } + \`); + + if (!posts || posts.length === 0) { + return; + } + + const [post] = posts; + + const { insert_comments: { returning: { id } } } = await context.graphql(\` + mutation { + insert_comments( + objects: {account_id: "morgs.near", block_height: \${block.blockHeight}, content: "cool post", post_id: \${post.id}} + ) { + returning { + id + } + } + } + \`); + + return (\`Created comment \${id} on post \${post.id}\`) + ` + }; + + await indexer.runFunctions(blockHeight, functions, false); + + expect(mockFetch.mock.calls).toMatchSnapshot(); + }); + + test('Indexer.runFunctions() console.logs', async () => { + const logs: string[] = []; + const context = { + log: (...m: string[]) => { + logs.push(...m); + } + }; + const vm = new VM(); + vm.freeze(context, 'context'); + vm.freeze(context, 'console'); + await vm.run('console.log("hello", "brave new"); context.log("world")'); + expect(logs).toEqual(['hello', 'brave new', 'world']); + }); + + test('Errors thrown in VM can be caught outside the VM', async () => { + const vm = new VM(); + expect(() => { + vm.run("throw new Error('boom')"); + }).toThrow('boom'); + }); + + test('Indexer.runFunctions() catches errors', async () => { + const mockFetch = jest.fn(() => ({ + status: 200, + json: async () => ({ + errors: null, + }), + })); + const blockHeight = 456; + const mockS3 = { + getObject: jest.fn(() => ({ + promise: async () => await Promise.resolve({ + Body: { + toString: () => JSON.stringify({ + chunks: [], + header: { + height: blockHeight + } + }) + } + }) + })), + } as unknown as AWS.S3; + const indexer = new Indexer('mainnet', { fetch: mockFetch as unknown as typeof fetch, s3: mockS3 }); + + const functions: Record = {}; + functions['buildnear.testnet/test'] = { + code: ` + throw new Error('boom'); + ` + }; + + await expect(indexer.runFunctions(blockHeight, functions, false)).rejects.toThrow(new Error('boom')); + expect(mockFetch.mock.calls).toMatchSnapshot(); + }); + + test('Indexer.runFunctions() provisions a GraphQL endpoint with the specified schema', async () => { + const blockHeight = 82699904; + const mockFetch = jest.fn(() => ({ + status: 200, + json: async () => ({ + errors: null, + }), + })); + const mockS3 = { + getObject: jest + .fn() + .mockReturnValueOnce({ // block + promise: async () => await Promise.resolve({ + Body: { + toString: () => JSON.stringify({ + chunks: [0], + header: { + height: blockHeight, + }, + }), + }, + }), + }) + .mockReturnValue({ // shard + promise: async () => await Promise.resolve({ + Body: { + toString: () => JSON.stringify({}) + }, + }), + }), + } as unknown as AWS.S3; + const provisioner: any = { + isUserApiProvisioned: jest.fn().mockReturnValue(false), + provisionUserApi: jest.fn(), + }; + const indexer = new Indexer('mainnet', { fetch: mockFetch as unknown as typeof fetch, s3: mockS3, provisioner }); + + const functions = { + 'morgs.near/test': { + account_id: 'morgs.near', + function_name: 'test', + code: '', + schema: 'schema', + } + }; + await indexer.runFunctions(1, functions, false, { provision: true }); + + expect(provisioner.isUserApiProvisioned).toHaveBeenCalledWith('morgs.near', 'test'); + expect(provisioner.provisionUserApi).toHaveBeenCalledTimes(1); + expect(provisioner.provisionUserApi).toHaveBeenCalledWith( + 'morgs.near', + 'test', + 'schema' + ); + }); + + test('Indexer.runFunctions() skips provisioning if the endpoint exists', async () => { + const blockHeight = 82699904; + const mockFetch = jest.fn(() => ({ + status: 200, + json: async () => ({ + errors: null, + }), + })); + const mockS3 = { + getObject: jest + .fn() + .mockReturnValueOnce({ // block + promise: async () => await Promise.resolve({ + Body: { + toString: () => JSON.stringify({ + chunks: [0], + header: { + height: blockHeight, + }, + }), + }, + }), + }) + .mockReturnValue({ // shard + promise: async () => await Promise.resolve({ + Body: { + toString: () => JSON.stringify({}) + }, + }), + }), + } as unknown as AWS.S3; + const provisioner: any = { + isUserApiProvisioned: jest.fn().mockReturnValue(true), + provisionUserApi: jest.fn(), + }; + const indexer = new Indexer('mainnet', { fetch: mockFetch as unknown as typeof fetch, s3: mockS3, provisioner }); + + const functions: Record = { + 'morgs.near/test': { + code: '', + schema: 'schema', + } + }; + await indexer.runFunctions(1, functions, false, { provision: true }); + + expect(provisioner.provisionUserApi).not.toHaveBeenCalled(); + }); + + test('Indexer.runFunctions() supplies the required role to the GraphQL endpoint', async () => { + const blockHeight = 82699904; + const mockFetch = jest.fn(() => ({ + status: 200, + json: async () => ({ + errors: null, + }), + })); + const mockS3 = { + getObject: jest + .fn() + .mockReturnValueOnce({ // block + promise: async () => await Promise.resolve({ + Body: { + toString: () => JSON.stringify({ + chunks: [0], + header: { + height: blockHeight, + }, + }), + }, + }), + }) + .mockReturnValue({ // shard + promise: async () => await Promise.resolve({ + Body: { + toString: () => JSON.stringify({}) + }, + }), + }), + } as unknown as AWS.S3; + const provisioner: any = { + isUserApiProvisioned: jest.fn().mockReturnValue(true), + provisionUserApi: jest.fn(), + }; + const indexer = new Indexer('mainnet', { fetch: mockFetch as unknown as typeof fetch, s3: mockS3, provisioner }); + + const functions: Record = { + 'morgs.near/test': { + code: ` + context.graphql(\`mutation { set(functionName: "buildnear.testnet/test", key: "height", data: "\${block.blockHeight}")}\`); + `, + schema: 'schema', + } + }; + await indexer.runFunctions(blockHeight, functions, false, { provision: true }); + + expect(provisioner.provisionUserApi).not.toHaveBeenCalled(); + expect(mockFetch.mock.calls).toMatchSnapshot(); + }); + + test('Indexer.runFunctions() logs provisioning failures', async () => { + const blockHeight = 82699904; + const mockFetch = jest.fn(() => ({ + status: 200, + json: async () => ({ + errors: null, + }), + })); + const mockS3 = { + getObject: jest + .fn() + .mockReturnValueOnce({ // block + promise: async () => await Promise.resolve({ + Body: { + toString: () => JSON.stringify({ + chunks: [0], + header: { + height: blockHeight, + }, + }), + }, + }), + }) + .mockReturnValue({ // shard + promise: async () => await Promise.resolve({ + Body: { + toString: () => JSON.stringify({}) + }, + }), + }), + } as unknown as AWS.S3; + const error = new Error('something went wrong with provisioning'); + const provisioner: any = { + isUserApiProvisioned: jest.fn().mockReturnValue(false), + provisionUserApi: jest.fn().mockRejectedValue(error), + }; + const indexer = new Indexer('mainnet', { fetch: mockFetch as unknown as typeof fetch, s3: mockS3, provisioner }); + + const functions: Record = { + 'morgs.near/test': { + code: ` + context.graphql(\`mutation { set(functionName: "buildnear.testnet/test", key: "height", data: "\${block.blockHeight}")}\`); + `, + schema: 'schema', + } + }; + + await expect(indexer.runFunctions(blockHeight, functions, false, { provision: true })).rejects.toThrow(error); + expect(mockFetch.mock.calls).toMatchSnapshot(); + }); + + test('does not attach the hasura admin secret header when no role specified', async () => { + const mockFetch = jest.fn() + .mockResolvedValueOnce({ + status: 200, + json: async () => ({ + data: {} + }) + }); + const indexer = new Indexer('mainnet', { fetch: mockFetch as unknown as typeof fetch }); + // @ts-expect-error legacy test + const context = indexer.buildContext('test', 'morgs.near/test', 1, null); + + const mutation = ` + mutation { + newGreeting(greeting: "howdy") { + success + } + } + `; + + await context.graphql(mutation); + + expect(mockFetch.mock.calls[0]).toEqual([ + `${HASURA_ENDPOINT}/v1/graphql`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hasura-Use-Backend-Only-Permissions': 'true', + }, + body: JSON.stringify({ query: mutation }) + } + ]); + }); + + test('attaches the backend only header to requests to hasura', async () => { + const mockFetch = jest.fn() + .mockResolvedValueOnce({ + status: 200, + json: async () => ({ + data: {} + }) + }); + const role = 'morgs_near'; + const indexer = new Indexer('mainnet', { fetch: mockFetch as unknown as typeof fetch }); + const context = indexer.buildContext('test', 'morgs.near/test', 1, role); + + const mutation = ` + mutation { + newGreeting(greeting: "howdy") { + success + } + } + `; + + await context.graphql(mutation); + + expect(mockFetch.mock.calls[0]).toEqual([ + `${HASURA_ENDPOINT}/v1/graphql`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hasura-Use-Backend-Only-Permissions': 'true', + 'X-Hasura-Role': role, + 'X-Hasura-Admin-Secret': HASURA_ADMIN_SECRET + }, + body: JSON.stringify({ query: mutation }) + } + ]); + }); +}); diff --git a/runner/src/indexer/indexer.ts b/runner/src/indexer/indexer.ts new file mode 100644 index 000000000..4e7c743e1 --- /dev/null +++ b/runner/src/indexer/indexer.ts @@ -0,0 +1,353 @@ +import fetch, { type Response } from 'node-fetch'; +import { VM } from 'vm2'; +import AWS from 'aws-sdk'; +import { Block } from '@near-lake/primitives'; + +import Provisioner from '../provisioner'; + +interface Dependencies { + fetch: typeof fetch + s3: AWS.S3 + provisioner: Provisioner +}; + +interface Context { + graphql: (operation: string, variables?: Record) => Promise + set: (key: string, value: any) => Promise + log: (...log: any[]) => Promise + fetchFromSocialApi: (path: string, options?: any) => Promise +} + +interface IndexerFunction { + account_id: string + function_name: string + provisioned?: boolean + schema: string + code: string +} + +export default class Indexer { + DEFAULT_HASURA_ROLE; + + private readonly deps: Dependencies; + + constructor ( + private readonly network: string, + deps?: Partial + ) { + this.DEFAULT_HASURA_ROLE = 'append'; + this.network = network; + this.deps = { + fetch, + s3: new AWS.S3({ region: process.env.REGION }), + provisioner: new Provisioner(), + ...deps, + }; + } + + async runFunctions ( + blockHeight: number, + functions: Record, + isHistorical: boolean, + options: { provision?: boolean } = { provision: false } + ): Promise { + const blockWithHelpers = Block.fromStreamerMessage(await this.fetchStreamerMessage(blockHeight)); + + const lag = Date.now() - Math.floor(Number(blockWithHelpers.header().timestampNanosec) / 1000000); + + const simultaneousPromises: Array> = []; + const allMutations: string[] = []; + + for (const functionName in functions) { + try { + const indexerFunction = functions[functionName]; + + const runningMessage = `Running function ${functionName}` + (isHistorical ? ' historical backfill' : `, lag is: ${lag?.toString()}ms from block timestamp`); + console.log(runningMessage); // Print the running message to the console (Lambda logs) + + simultaneousPromises.push(this.writeLog(functionName, blockHeight, runningMessage)); + + const hasuraRoleName = functionName.split('/')[0].replace(/[.-]/g, '_'); + const functionNameWithoutAccount = functionName.split('/')[1].replace(/[.-]/g, '_'); + + if (options.provision && !indexerFunction.provisioned) { + try { + if (!await this.deps.provisioner.isUserApiProvisioned(indexerFunction.account_id, indexerFunction.function_name)) { + await this.setStatus(functionName, blockHeight, 'PROVISIONING'); + simultaneousPromises.push(this.writeLog(functionName, blockHeight, 'Provisioning endpoint: starting')); + + await this.deps.provisioner.provisionUserApi(indexerFunction.account_id, indexerFunction.function_name, indexerFunction.schema); + + simultaneousPromises.push(this.writeLog(functionName, blockHeight, 'Provisioning endpoint: successful')); + } + } catch (e) { + const error = e as Error; + simultaneousPromises.push(this.writeLog(functionName, blockHeight, 'Provisioning endpoint: failure', error.message)); + throw error; + } + } + + await this.setStatus(functionName, blockHeight, 'RUNNING'); + + const vm = new VM({ timeout: 3000, allowAsync: true }); + const context = this.buildContext(functionName, functionNameWithoutAccount, blockHeight, hasuraRoleName); + + vm.freeze(blockWithHelpers, 'block'); + vm.freeze(context, 'context'); + vm.freeze(context, 'console'); // provide console.log via context.log + + const modifiedFunction = this.transformIndexerFunction(indexerFunction.code); + try { + await vm.run(modifiedFunction); + } catch (e) { + const error = e as Error; + // NOTE: logging the exception would likely leak some information about the index runner. + // For now, we just log the message. In the future we could sanitize the stack trace + // and give the correct line number offsets within the indexer function + console.error(`${functionName}: Error running IndexerFunction on block ${blockHeight}: ${error.message}`); + await this.writeLog(functionName, blockHeight, 'Error running IndexerFunction', error.message); + throw e; + } + + simultaneousPromises.push(this.writeFunctionState(functionName, blockHeight, isHistorical)); + } catch (e) { + console.error(`${functionName}: Failed to run function`, e); + await this.setStatus(functionName, blockHeight, 'STOPPED'); + throw e; + } finally { + await Promise.all(simultaneousPromises); + } + } + return allMutations; + } + + // pad with 0s to 12 digits + normalizeBlockHeight (blockHeight: number): string { + return blockHeight.toString().padStart(12, '0'); + } + + async fetchStreamerMessage (blockHeight: number): Promise<{ block: any, shards: any[] }> { + const blockPromise = this.fetchBlockPromise(blockHeight); + const shardsPromises = await this.fetchShardsPromises(blockHeight, 4); + + const results = await Promise.all([blockPromise, ...shardsPromises]); + const block = results.shift(); + const shards = results; + return { + block, + shards, + }; + } + + async fetchShardsPromises (blockHeight: number, numberOfShards: number): Promise>> { + return ([...Array(numberOfShards).keys()].map(async (shardId) => + await this.fetchShardPromise(blockHeight, shardId) + )); + } + + async fetchShardPromise (blockHeight: number, shardId: number): Promise { + const params = { + Bucket: `near-lake-data-${this.network}`, + Key: `${this.normalizeBlockHeight(blockHeight)}/shard_${shardId}.json`, + }; + return await this.deps.s3.getObject(params).promise().then((response) => { + return JSON.parse(response.Body?.toString() ?? '{}', (_key, value) => this.renameUnderscoreFieldsToCamelCase(value)); + }); + } + + async fetchBlockPromise (blockHeight: number): Promise { + const file = 'block.json'; + const folder = this.normalizeBlockHeight(blockHeight); + const params = { + Bucket: 'near-lake-data-' + this.network, + Key: `${folder}/${file}`, + }; + return await this.deps.s3.getObject(params).promise().then((response) => { + const block = JSON.parse(response.Body?.toString() ?? '{}', (_key, value) => this.renameUnderscoreFieldsToCamelCase(value)); + return block; + }); + } + + enableAwaitTransform (indexerFunction: string): string { + return ` + async function f(){ + ${indexerFunction} + }; + f(); + `; + } + + transformIndexerFunction (indexerFunction: string): string { + return [ + this.enableAwaitTransform, + ].reduce((acc, val) => val(acc), indexerFunction); + } + + buildContext (functionName: string, functionNameWithoutAccount: string, blockHeight: number, hasuraRoleName: string): Context { + return { + graphql: async (operation, variables) => { + console.log(`${functionName}: Running context graphql`, operation); + return await this.runGraphQLQuery(operation, variables, functionName, blockHeight, hasuraRoleName); + }, + set: async (key, value) => { + const mutation = + `mutation SetKeyValue($function_name: String!, $key: String!, $value: String!) { + insert_${hasuraRoleName}_${functionNameWithoutAccount}_indexer_storage_one(object: {function_name: $function_name, key_name: $key, value: $value} on_conflict: {constraint: indexer_storage_pkey, update_columns: value}) {key_name} + }`; + const variables = { + function_name: functionName, + key, + value: value ? JSON.stringify(value) : null + }; + console.log(`${functionName}: Running set:`, mutation, variables); + return await this.runGraphQLQuery(mutation, variables, functionName, blockHeight, hasuraRoleName); + }, + log: async (...log) => { + return await this.writeLog(functionName, blockHeight, ...log); + }, + fetchFromSocialApi: async (path, options) => { + return await this.deps.fetch(`https://api.near.social${path}`, options); + } + }; + } + + async setStatus (functionName: string, blockHeight: number, status: string): Promise { + return await this.runGraphQLQuery( + ` + mutation SetStatus($function_name: String, $status: String) { + insert_indexer_state_one(object: {function_name: $function_name, status: $status, current_block_height: 0 }, on_conflict: { constraint: indexer_state_pkey, update_columns: status }) { + function_name + status + } + } + `, + { + function_name: functionName, + status, + }, + functionName, + blockHeight, + this.DEFAULT_HASURA_ROLE + ); + } + + async writeLog (functionName: string, blockHeight: number, ...message: any[]): Promise { + const parsedMessage: string = message + .map(m => typeof m === 'object' ? JSON.stringify(m) : m) + .join(':'); + + const mutation = + `mutation writeLog($function_name: String!, $block_height: numeric!, $message: String!){ + insert_indexer_log_entries_one(object: {function_name: $function_name, block_height: $block_height, message: $message}) {id} + }`; + + return await this.runGraphQLQuery(mutation, { function_name: functionName, block_height: blockHeight, message: parsedMessage }, + functionName, blockHeight, this.DEFAULT_HASURA_ROLE) + .then((result: any) => { + return result?.insert_indexer_log_entries_one?.id; + }) + .catch((e: any) => { + console.error(`${functionName}: Error writing log`, e); + }); + } + + async writeFunctionState (functionName: string, blockHeight: number, isHistorical: boolean): Promise { + const realTimeMutation: string = + `mutation WriteBlock($function_name: String!, $block_height: numeric!) { + insert_indexer_state( + objects: {current_block_height: $block_height, function_name: $function_name} + on_conflict: {constraint: indexer_state_pkey, update_columns: current_block_height} + ) { + returning { + current_block_height + function_name + } + } + }`; + const historicalMutation: string = ` + mutation WriteBlock($function_name: String!, $block_height: numeric!) { + insert_indexer_state( + objects: {current_historical_block_height: $block_height, current_block_height: 0, function_name: $function_name} + on_conflict: {constraint: indexer_state_pkey, update_columns: current_historical_block_height} + ) { + returning { + current_block_height + current_historical_block_height + function_name + } + } + } + `; + const variables: any = { + function_name: functionName, + block_height: blockHeight, + }; + return await this.runGraphQLQuery(isHistorical ? historicalMutation : realTimeMutation, variables, functionName, blockHeight, this.DEFAULT_HASURA_ROLE) + .catch((e: any) => { + console.error(`${functionName}: Error writing function state`, e); + }); + } + + async runGraphQLQuery (operation: string, variables: any, functionName: string, blockHeight: number, hasuraRoleName: string | null, logError: boolean = true): Promise { + const response: Response = await this.deps.fetch(`${process.env.HASURA_ENDPOINT}/v1/graphql`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Hasura-Use-Backend-Only-Permissions': 'true', + ...(hasuraRoleName && { + 'X-Hasura-Role': hasuraRoleName, + 'X-Hasura-Admin-Secret': process.env.HASURA_ADMIN_SECRET + }), + }, + body: JSON.stringify({ + query: operation, + ...(variables && { variables }), + }), + }); + + const { data, errors } = await response.json(); + + if (response.status !== 200 || errors) { + if (logError) { + console.log(`${functionName}: Error writing graphql `, errors); // temporary extra logging + + const message: string = errors ? errors.map((e: any) => e.message).join(', ') : `HTTP ${response.status} error writing with graphql to indexer storage`; + const mutation: string = + `mutation writeLog($function_name: String!, $block_height: numeric!, $message: String!){ + insert_indexer_log_entries_one(object: {function_name: $function_name, block_height: $block_height, message: $message}) { + id + } + }`; + try { + await this.runGraphQLQuery(mutation, { function_name: functionName, block_height: blockHeight, message }, functionName, blockHeight, this.DEFAULT_HASURA_ROLE, false); + } catch (e) { + console.error(`${functionName}: Error writing log of graphql error`, e); + } + } + throw new Error(`Failed to write graphql, http status: ${response.status}, errors: ${JSON.stringify(errors, null, 2)}`); + } + + return data; + } + + renameUnderscoreFieldsToCamelCase (value: Record): Record { + if (typeof value === 'object' && !Array.isArray(value)) { + // It's a non-null, non-array object, create a replacement with the keys initially-capped + const newValue: any = {}; + for (const key in value) { + const newKey: string = key + .split('_') + .map((word, i) => { + if (i > 0) { + return word.charAt(0).toUpperCase() + word.slice(1); + } + return word; + }) + .join(''); + newValue[newKey] = value[key]; + } + return newValue; + } + return value; + } +} diff --git a/runner/src/pg-client.ts b/runner/src/pg-client.ts new file mode 100644 index 000000000..ebca73a49 --- /dev/null +++ b/runner/src/pg-client.ts @@ -0,0 +1,41 @@ +import { Pool, type PoolConfig, type QueryResult, type QueryResultRow } from 'pg'; +import pgFormatModule from 'pg-format'; + +interface ConnectionParams { + user: string + password: string + host: string + port: number | string + database: string +} + +export default class PgClient { + private readonly pgPool: Pool; + public format: typeof pgFormatModule; + + constructor ( + connectionParams: ConnectionParams, + poolConfig: PoolConfig = { max: 10, idleTimeoutMillis: 30000 }, + PgPool: typeof Pool = Pool, + pgFormat: typeof pgFormatModule = pgFormatModule + ) { + this.pgPool = new PgPool({ + user: connectionParams.user, + password: connectionParams.password, + host: connectionParams.host, + port: Number(connectionParams.port), + database: connectionParams.database, + ...poolConfig, + }); + this.format = pgFormat; + } + + async query(query: string, params: any[] = []): Promise> { + const client = await this.pgPool.connect(); + try { + return await (client.query(query, params)); + } finally { + client.release(); + } + } +} diff --git a/runner/src/provisioner/__snapshots__/provisioner.test.ts.snap b/runner/src/provisioner/__snapshots__/provisioner.test.ts.snap new file mode 100644 index 000000000..406ca3cc0 --- /dev/null +++ b/runner/src/provisioner/__snapshots__/provisioner.test.ts.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Provisioner provisionUserApi formats user input before executing the query 1`] = ` +[ + [ + "CREATE DATABASE "databaseName UNION SELECT * FROM users --"", + ], + [ + "CREATE USER morgs_near WITH PASSWORD 'pass; DROP TABLE users;--'", + ], + [ + "GRANT ALL PRIVILEGES ON DATABASE "databaseName UNION SELECT * FROM users --" TO morgs_near", + ], + [ + "REVOKE CONNECT ON DATABASE "databaseName UNION SELECT * FROM users --" FROM PUBLIC", + ], +] +`; diff --git a/runner/src/provisioner/index.ts b/runner/src/provisioner/index.ts new file mode 100644 index 000000000..5a4dbeb8d --- /dev/null +++ b/runner/src/provisioner/index.ts @@ -0,0 +1 @@ +export { default } from './provisioner'; diff --git a/runner/src/provisioner/provisioner.test.ts b/runner/src/provisioner/provisioner.test.ts new file mode 100644 index 000000000..a1c6cbc62 --- /dev/null +++ b/runner/src/provisioner/provisioner.test.ts @@ -0,0 +1,212 @@ +import pgFormat from 'pg-format'; + +import Provisioner from './provisioner'; + +describe('Provisioner', () => { + let pgClient: any; + let hasuraClient: any; + + const tableNames = ['blocks']; + const accountId = 'morgs.near'; + const sanitizedAccountId = 'morgs_near'; + const functionName = 'test-function'; + const sanitizedFunctionName = 'test_function'; + const databaseSchema = 'CREATE TABLE blocks (height numeric)'; + const error = new Error('some error'); + const defaultDatabase = 'default'; + const schemaName = `${sanitizedAccountId}_${sanitizedFunctionName}`; + + const password = 'password'; + const crypto: any = { + randomBytes: () => ({ + toString: () => ({ + slice: () => ({ + replace: () => password, + }), + }), + }), + }; + + beforeEach(() => { + hasuraClient = { + getTableNames: jest.fn().mockReturnValueOnce(tableNames), + trackTables: jest.fn().mockReturnValueOnce(null), + trackForeignKeyRelationships: jest.fn().mockReturnValueOnce(null), + addPermissionsToTables: jest.fn().mockReturnValueOnce(null), + addDatasource: jest.fn().mockReturnValueOnce(null), + runMigrations: jest.fn().mockReturnValueOnce(null), + createSchema: jest.fn().mockReturnValueOnce(null), + doesSourceExist: jest.fn().mockReturnValueOnce(false), + doesSchemaExist: jest.fn().mockReturnValueOnce(false), + untrackTables: jest.fn().mockReturnValueOnce(null), + }; + + pgClient = { + query: jest.fn().mockReturnValue(null), + format: pgFormat, + }; + }); + + describe('isUserApiProvisioned', () => { + it('returns false if datasource doesnt exists', async () => { + hasuraClient.doesSourceExist = jest.fn().mockReturnValueOnce(false); + + const provisioner = new Provisioner(hasuraClient, pgClient, crypto); + + await expect(provisioner.isUserApiProvisioned(accountId, functionName)).resolves.toBe(false); + }); + + it('returns false if datasource and schema dont exists', async () => { + hasuraClient.doesSourceExist = jest.fn().mockReturnValueOnce(false); + hasuraClient.doesSchemaExist = jest.fn().mockReturnValueOnce(false); + + const provisioner = new Provisioner(hasuraClient, pgClient, crypto); + + await expect(provisioner.isUserApiProvisioned(accountId, functionName)).resolves.toBe(false); + }); + + it('returns true if datasource and schema exists', async () => { + hasuraClient.doesSourceExist = jest.fn().mockReturnValueOnce(true); + hasuraClient.doesSchemaExist = jest.fn().mockReturnValueOnce(true); + + const provisioner = new Provisioner(hasuraClient, pgClient, crypto); + + await expect(provisioner.isUserApiProvisioned(accountId, functionName)).resolves.toBe(true); + }); + }); + + describe('provisionUserApi', () => { + it('provisions an API for the user', async () => { + const provisioner = new Provisioner(hasuraClient, pgClient, crypto); + + await provisioner.provisionUserApi(accountId, functionName, databaseSchema); + + expect(pgClient.query.mock.calls).toEqual([ + ['CREATE DATABASE morgs_near'], + ['CREATE USER morgs_near WITH PASSWORD \'password\''], + ['GRANT ALL PRIVILEGES ON DATABASE morgs_near TO morgs_near'], + ['REVOKE CONNECT ON DATABASE morgs_near FROM PUBLIC'], + ]); + expect(hasuraClient.addDatasource).toBeCalledWith(sanitizedAccountId, password, sanitizedAccountId); + expect(hasuraClient.createSchema).toBeCalledWith(sanitizedAccountId, schemaName); + expect(hasuraClient.runMigrations).toBeCalledWith(sanitizedAccountId, schemaName, databaseSchema); + expect(hasuraClient.getTableNames).toBeCalledWith(schemaName, sanitizedAccountId); + expect(hasuraClient.trackTables).toBeCalledWith(schemaName, tableNames, sanitizedAccountId); + expect(hasuraClient.addPermissionsToTables).toBeCalledWith( + schemaName, + sanitizedAccountId, + tableNames, + sanitizedAccountId, + [ + 'select', + 'insert', + 'update', + 'delete' + ] + ); + }); + + it('untracks tables from the previous schema if they exists', async () => { + hasuraClient.doesSchemaExist = jest.fn().mockReturnValueOnce(true); + + const provisioner = new Provisioner(hasuraClient, pgClient, crypto); + + await provisioner.provisionUserApi(accountId, functionName, databaseSchema); + + expect(hasuraClient.getTableNames).toBeCalledWith(schemaName, defaultDatabase); + expect(hasuraClient.untrackTables).toBeCalledWith(defaultDatabase, schemaName, tableNames); + }); + + it('skips provisioning the datasource if it already exists', async () => { + hasuraClient.doesSourceExist = jest.fn().mockReturnValueOnce(true); + + const provisioner = new Provisioner(hasuraClient, pgClient, crypto); + + await provisioner.provisionUserApi(accountId, functionName, databaseSchema); + + expect(pgClient.query).not.toBeCalled(); + expect(hasuraClient.addDatasource).not.toBeCalled(); + + expect(hasuraClient.createSchema).toBeCalledWith(sanitizedAccountId, schemaName); + expect(hasuraClient.runMigrations).toBeCalledWith(sanitizedAccountId, schemaName, databaseSchema); + expect(hasuraClient.getTableNames).toBeCalledWith(schemaName, sanitizedAccountId); + expect(hasuraClient.trackTables).toBeCalledWith(schemaName, tableNames, sanitizedAccountId); + expect(hasuraClient.addPermissionsToTables).toBeCalledWith( + schemaName, + sanitizedAccountId, + tableNames, + sanitizedAccountId, + [ + 'select', + 'insert', + 'update', + 'delete' + ] + ); + }); + + it('formats user input before executing the query', async () => { + const provisioner = new Provisioner(hasuraClient, pgClient, crypto); + + await provisioner.createUserDb('morgs_near', 'pass; DROP TABLE users;--', 'databaseName UNION SELECT * FROM users --'); + + expect(pgClient.query.mock.calls).toMatchSnapshot(); + }); + + it('throws an error when it fails to create a postgres db', async () => { + pgClient.query = jest.fn().mockRejectedValue(error); + + const provisioner = new Provisioner(hasuraClient, pgClient, crypto); + + await expect(provisioner.provisionUserApi(accountId, functionName, databaseSchema)).rejects.toThrow('Failed to provision endpoint: Failed to create user db: some error'); + }); + + it('throws an error when it fails to add the db to hasura', async () => { + hasuraClient.addDatasource = jest.fn().mockRejectedValue(error); + + const provisioner = new Provisioner(hasuraClient, pgClient, crypto); + + await expect(provisioner.provisionUserApi(accountId, functionName, databaseSchema)).rejects.toThrow('Failed to provision endpoint: Failed to add datasource: some error'); + }); + + it('throws an error when it fails to run migrations', async () => { + hasuraClient.runMigrations = jest.fn().mockRejectedValue(error); + + const provisioner = new Provisioner(hasuraClient, pgClient, crypto); + + await expect(provisioner.provisionUserApi(accountId, functionName, databaseSchema)).rejects.toThrow('Failed to provision endpoint: Failed to run migrations: some error'); + }); + + it('throws an error when it fails to fetch table names', async () => { + hasuraClient.getTableNames = jest.fn().mockRejectedValue(error); + + const provisioner = new Provisioner(hasuraClient, pgClient, crypto); + + await expect(provisioner.provisionUserApi(accountId, functionName, databaseSchema)).rejects.toThrow('Failed to provision endpoint: Failed to fetch table names: some error'); + }); + + it('throws an error when it fails to track tables', async () => { + hasuraClient.trackTables = jest.fn().mockRejectedValue(error); + + const provisioner = new Provisioner(hasuraClient, pgClient, crypto); + + await expect(provisioner.provisionUserApi(accountId, functionName, databaseSchema)).rejects.toThrow('Failed to provision endpoint: Failed to track tables: some error'); + }); + + it('throws an error when it fails to track foreign key relationships', async () => { + hasuraClient.trackForeignKeyRelationships = jest.fn().mockRejectedValue(error); + + const provisioner = new Provisioner(hasuraClient, pgClient, crypto); + + await expect(provisioner.provisionUserApi(accountId, functionName, databaseSchema)).rejects.toThrow('Failed to provision endpoint: Failed to track foreign key relationships: some error'); + }); + + it('throws an error when it fails to add permissions to tables', async () => { + hasuraClient.addPermissionsToTables = jest.fn().mockRejectedValue(error); + + const provisioner = new Provisioner(hasuraClient, pgClient, crypto); + + await expect(provisioner.provisionUserApi(accountId, functionName, databaseSchema)).rejects.toThrow('Failed to provision endpoint: Failed to add permissions to tables: some error'); + }); + }); +}); diff --git a/runner/src/provisioner/provisioner.ts b/runner/src/provisioner/provisioner.ts new file mode 100644 index 000000000..a0e42495e --- /dev/null +++ b/runner/src/provisioner/provisioner.ts @@ -0,0 +1,161 @@ +import VError from 'verror'; +import cryptoModule from 'crypto'; +import HasuraClient from '../hasura-client'; +import PgClient from '../pg-client'; + +const DEFAULT_PASSWORD_LENGTH = 16; + +const sharedPgClient = new PgClient({ + user: process.env.PG_ADMIN_USER, + password: process.env.PG_ADMIN_PASSWORD, + database: process.env.PG_ADMIN_DATABASE, + host: process.env.PG_HOST, + port: Number(process.env.PG_PORT), +}); + +export default class Provisioner { + constructor ( + private readonly hasuraClient: HasuraClient = new HasuraClient(), + private readonly pgClient: PgClient = sharedPgClient, + private readonly crypto: typeof cryptoModule = cryptoModule, + ) { + this.hasuraClient = hasuraClient; + this.pgClient = pgClient; + this.crypto = crypto; + } + + generatePassword (length: number = DEFAULT_PASSWORD_LENGTH): string { + return this.crypto + .randomBytes(length) + .toString('base64') + .slice(0, length) + .replace(/\+/g, '0') + .replace(/\//g, '0'); + } + + async createDatabase (name: string): Promise { + await this.pgClient.query(this.pgClient.format('CREATE DATABASE %I', name)); + } + + async createUser (name: string, password: string): Promise { + await this.pgClient.query(this.pgClient.format('CREATE USER %I WITH PASSWORD %L', name, password)); + } + + async restrictUserToDatabase (databaseName: string, userName: string): Promise { + await this.pgClient.query(this.pgClient.format('GRANT ALL PRIVILEGES ON DATABASE %I TO %I', databaseName, userName)); + await this.pgClient.query(this.pgClient.format('REVOKE CONNECT ON DATABASE %I FROM PUBLIC', databaseName)); + } + + async createUserDb (userName: string, password: string, databaseName: string): Promise { + await this.wrapError( + async () => { + await this.createDatabase(databaseName); + await this.createUser(userName, password); + await this.restrictUserToDatabase(databaseName, userName); + }, + 'Failed to create user db' + ); + } + + async isUserApiProvisioned (accountId: string, functionName: string): Promise { + const sanitizedAccountId = this.replaceSpecialChars(accountId); + const sanitizedFunctionName = this.replaceSpecialChars(functionName); + + const databaseName = sanitizedAccountId; + const schemaName = `${sanitizedAccountId}_${sanitizedFunctionName}`; + + const sourceExists = await this.hasuraClient.doesSourceExist(databaseName); + if (!sourceExists) { + return false; + } + + const schemaExists = await this.hasuraClient.doesSchemaExist(databaseName, schemaName); + + return schemaExists; + } + + async wrapError(fn: () => Promise, errorMessage: string): Promise { + try { + return await fn(); + } catch (error) { + if (error instanceof Error) { + throw new VError(error, errorMessage); + } + throw new VError(errorMessage); + } + } + + async createSchema (databaseName: string, schemaName: string): Promise { + return await this.wrapError(async () => await this.hasuraClient.createSchema(databaseName, schemaName), 'Failed to create schema'); + } + + async runMigrations (databaseName: string, schemaName: string, migration: any): Promise { + return await this.wrapError(async () => await this.hasuraClient.runMigrations(databaseName, schemaName, migration), 'Failed to run migrations'); + } + + async getTableNames (schemaName: string, databaseName: string): Promise { + return await this.wrapError(async () => await this.hasuraClient.getTableNames(schemaName, databaseName), 'Failed to fetch table names'); + } + + async trackTables (schemaName: string, tableNames: string[], databaseName: string): Promise { + return await this.wrapError(async () => await this.hasuraClient.trackTables(schemaName, tableNames, databaseName), 'Failed to track tables'); + } + + async addPermissionsToTables (schemaName: string, databaseName: string, tableNames: string[], roleName: string, permissions: string[]): Promise { + return await this.wrapError(async () => await this.hasuraClient.addPermissionsToTables( + schemaName, + databaseName, + tableNames, + roleName, + permissions + ), 'Failed to add permissions to tables'); + } + + async trackForeignKeyRelationships (schemaName: string, databaseName: string): Promise { + return await this.wrapError(async () => await this.hasuraClient.trackForeignKeyRelationships(schemaName, databaseName), 'Failed to track foreign key relationships'); + } + + async addDatasource (userName: string, password: string, databaseName: string): Promise { + return await this.wrapError(async () => await this.hasuraClient.addDatasource(userName, password, databaseName), 'Failed to add datasource'); + } + + replaceSpecialChars (str: string): string { + return str.replaceAll(/[.-]/g, '_'); + } + + async provisionUserApi (accountId: string, functionName: string, databaseSchema: any): Promise { // replace any with actual type + const sanitizedAccountId = this.replaceSpecialChars(accountId); + const sanitizedFunctionName = this.replaceSpecialChars(functionName); + + const databaseName = sanitizedAccountId; + const userName = sanitizedAccountId; + const schemaName = `${sanitizedAccountId}_${sanitizedFunctionName}`; + + await this.wrapError( + async () => { + if (!await this.hasuraClient.doesSourceExist(databaseName)) { + const password = this.generatePassword(); + await this.createUserDb(userName, password, databaseName); + await this.addDatasource(userName, password, databaseName); + } + + // Untrack tables from old schema to prevent conflicts with new DB + if (await this.hasuraClient.doesSchemaExist(HasuraClient.DEFAULT_DATABASE, schemaName)) { + const tableNames = await this.getTableNames(schemaName, HasuraClient.DEFAULT_DATABASE); + await this.hasuraClient.untrackTables(HasuraClient.DEFAULT_DATABASE, schemaName, tableNames); + } + + await this.createSchema(databaseName, schemaName); + await this.runMigrations(databaseName, schemaName, databaseSchema); + + const tableNames = await this.getTableNames(schemaName, databaseName); + await this.trackTables(schemaName, tableNames, databaseName); + + await this.trackForeignKeyRelationships(schemaName, databaseName); + + await this.addPermissionsToTables(schemaName, databaseName, tableNames, userName, ['select', 'insert', 'update', 'delete']); + }, + 'Failed to provision endpoint' + ); + } +} diff --git a/runner/tsconfig.json b/runner/tsconfig.json new file mode 100644 index 000000000..c3aead636 --- /dev/null +++ b/runner/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "es2018", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": ["es2021"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + "module": "commonjs", /* Specify what module code is generated. */ + "rootDir": "src", /* Specify the root folder within your source files. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + "resolveJsonModule": true, /* Enable importing .json files. */ + "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + "outDir": "dist", /* Specify an output folder for all emitted files. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} From ca7aefd2e47b76f78da8df5b0ab45f9717c19d60 Mon Sep 17 00:00:00 2001 From: Morgan McCauley Date: Mon, 7 Aug 2023 15:00:37 +1200 Subject: [PATCH 02/31] DPLT-1084 Add support for Runner infrastructure (#166) --- runner/Dockerfile | 12 ++++++++++++ runner/package-lock.json | 6 ++++-- runner/package.json | 6 +++++- runner/src/globals.d.ts | 11 +++++------ runner/src/indexer/indexer.ts | 2 +- runner/src/provisioner/provisioner.ts | 10 +++++----- 6 files changed, 32 insertions(+), 15 deletions(-) create mode 100644 runner/Dockerfile diff --git a/runner/Dockerfile b/runner/Dockerfile new file mode 100644 index 000000000..883e0499b --- /dev/null +++ b/runner/Dockerfile @@ -0,0 +1,12 @@ +FROM node:18.17 AS builder +WORKDIR /usr/src/app +COPY . . +RUN npm install +RUN npm run build + +FROM node:18.17 +WORKDIR /usr/src/app +COPY --from=builder /usr/src/app/package*.json ./ +RUN npm install --omit=dev +COPY --from=builder /usr/src/app/dist ./dist +CMD [ "npm", "run", "start:docker" ] diff --git a/runner/package-lock.json b/runner/package-lock.json index e0bd77c3e..d65a5963c 100644 --- a/runner/package-lock.json +++ b/runner/package-lock.json @@ -14,6 +14,7 @@ "node-fetch": "^2.6.11", "pg": "^8.11.1", "pg-format": "^1.0.4", + "pluralize": "^8.0.0", "redis": "^4.6.7", "verror": "^1.10.1", "vm2": "^3.9.19" @@ -36,11 +37,13 @@ "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-promise": "^6.1.1", "jest": "^29.6.2", - "pluralize": "^8.0.0", "prettier": "^3.0.0", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", "typescript": "^5.1.6" + }, + "engines": { + "node": "18.17" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -5922,7 +5925,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", - "dev": true, "engines": { "node": ">=4" } diff --git a/runner/package.json b/runner/package.json index 0cfb5d41b..27e7f3a1a 100644 --- a/runner/package.json +++ b/runner/package.json @@ -3,10 +3,14 @@ "version": "1.0.0", "description": "", "main": "index.js", + "engines": { + "node": "18.17" + }, "scripts": { "build": "rm -rf ./dist && tsc", "start": "npm run build && node dist/index.js", "start:dev": "ts-node ./src/index.ts", + "start:docker": "node dist/index.js", "test": "node --experimental-vm-modules ./node_modules/.bin/jest", "lint": "eslint -c .eslintrc.js" }, @@ -31,7 +35,6 @@ "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-promise": "^6.1.1", "jest": "^29.6.2", - "pluralize": "^8.0.0", "prettier": "^3.0.0", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", @@ -39,6 +42,7 @@ }, "dependencies": { "@near-lake/primitives": "^0.1.0", + "pluralize": "^8.0.0", "aws-sdk": "^2.1402.0", "node-fetch": "^2.6.11", "pg": "^8.11.1", diff --git a/runner/src/globals.d.ts b/runner/src/globals.d.ts index d5ede3373..11e393de7 100644 --- a/runner/src/globals.d.ts +++ b/runner/src/globals.d.ts @@ -2,11 +2,10 @@ declare namespace NodeJS { export interface ProcessEnv { HASURA_ENDPOINT: string HASURA_ADMIN_SECRET: string - PG_HOST: string - PG_PORT: string - PG_ADMIN_USER: string - PG_ADMIN_PASSWORD: string - PG_ADMIN_DATABASE: string - REGION: string + PGHOST: string + PGPORT: string + PGUSER: string + PGPASSWORD: string + PGDATABASE: string } } diff --git a/runner/src/indexer/indexer.ts b/runner/src/indexer/indexer.ts index 4e7c743e1..6dc9326f2 100644 --- a/runner/src/indexer/indexer.ts +++ b/runner/src/indexer/indexer.ts @@ -39,7 +39,7 @@ export default class Indexer { this.network = network; this.deps = { fetch, - s3: new AWS.S3({ region: process.env.REGION }), + s3: new AWS.S3(), provisioner: new Provisioner(), ...deps, }; diff --git a/runner/src/provisioner/provisioner.ts b/runner/src/provisioner/provisioner.ts index a0e42495e..334b4dbf0 100644 --- a/runner/src/provisioner/provisioner.ts +++ b/runner/src/provisioner/provisioner.ts @@ -6,11 +6,11 @@ import PgClient from '../pg-client'; const DEFAULT_PASSWORD_LENGTH = 16; const sharedPgClient = new PgClient({ - user: process.env.PG_ADMIN_USER, - password: process.env.PG_ADMIN_PASSWORD, - database: process.env.PG_ADMIN_DATABASE, - host: process.env.PG_HOST, - port: Number(process.env.PG_PORT), + user: process.env.PGUSER, + password: process.env.PGPASSWORD, + database: process.env.PGDATABASE, + host: process.env.PGHOST, + port: Number(process.env.PGPORT), }); export default class Provisioner { From 6a113ed02dc02977ffa23a9c5ba269470a070d2e Mon Sep 17 00:00:00 2001 From: Roshaan Siddiqui Date: Mon, 7 Aug 2023 16:22:26 -0500 Subject: [PATCH 03/31] DPLT-999 feat: fetch activeTab from localstorage to persist-tabs (#162) --- frontend/widgets/src/QueryApi.Dashboard.jsx | 57 ++++++++++----------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/frontend/widgets/src/QueryApi.Dashboard.jsx b/frontend/widgets/src/QueryApi.Dashboard.jsx index 8b7c30f2e..b8a166e40 100644 --- a/frontend/widgets/src/QueryApi.Dashboard.jsx +++ b/frontend/widgets/src/QueryApi.Dashboard.jsx @@ -16,8 +16,9 @@ const EXTERNAL_APP_URL = props.EXTERNAL_APP_URL || "https://queryapi-frontend-24ktefolwq-ew.a.run.app"; let appPath = props.isDev ? "dev-App" : "App"; + State.init({ - activeTab: activeTab, + activeTab: Storage.privateGet("queryapi:activeTab") || activeTab, my_indexers: [], all_indexers: [], selected_indexer: undefined, @@ -283,6 +284,20 @@ const ButtonLink = styled.a` }} `; +const previousSelectedTab = Storage.privateGet("queryapi:activeTab"); +if (previousSelectedTab && previousSelectedTab !== state.activeTab) { + State.update({ + activeTab: previousSelectedTab, + }); +} + +const selectTab = (tabName) => { + Storage.privateSet("queryapi:activeTab", tabName); + State.update({ + activeTab: tabName, + }); +}; + const indexerView = (accountId, indexerName) => { const editUrl = `https://near.org/#/${APP_OWNER}/widget/QueryApi.${appPath}?selectedIndexerPath=${accountId}/${indexerName}&view=editor-window`; const statusUrl = `https://near.org/#/${APP_OWNER}/widget/QueryApi.${appPath}?selectedIndexerPath=${accountId}/${indexerName}&view=indexer-status`; @@ -318,23 +333,10 @@ const indexerView = (accountId, indexerName) => { - - State.update({ - activeTab: "indexer-status", - }) - } - > + selectTab("indexer-status")}> View Status - - State.update({ - activeTab: "editor-window", - }) - } - > + selectTab("editor-window")}> {accountId === context.accountId ? "Edit Indexer" : "View Indexer"} @@ -350,7 +352,7 @@ return ( State.update({ activeTab: "indexers" })} + onClick={() => selectTab("indexers")} selected={state.activeTab === "indexers"} > Indexers @@ -358,7 +360,7 @@ return ( {props.view === "create-new-indexer" && ( State.update({ activeTab: "create-new-indexer" })} + onClick={() => selectTab("create-new-indexer")} selected={state.activeTab === "create-new-indexer"} > Create New Indexer @@ -369,7 +371,7 @@ return ( <> State.update({ activeTab: "editor-window" })} + onClick={() => selectTab("editor-window")} selected={state.activeTab === "editor-window"} > Indexer Editor @@ -377,7 +379,7 @@ return ( State.update({ activeTab: "indexer-status" })} + onClick={() => selectTab("indexer-status")} selected={state.activeTab === "indexer-status"} > Indexer Status @@ -390,11 +392,7 @@ return ( { - State.update({ - activeTab: "indexers", - }); - }} + onClick={() => selectTab("indexers")} > + onClick={() => { State.update({ activeTab: "create-new-indexer", selected_indexer: "", - }) - } + }); + selectTab("create-new-indexer"); + }} > Create New Indexer @@ -474,7 +473,7 @@ return ( ))} {indexerView( selected_accountId ?? state.indexers[0].accountId, - selected_indexerName ?? state.indexers[0].indexerName, + selected_indexerName ?? state.indexers[0].indexerName )} Date: Mon, 7 Aug 2023 16:22:59 -0500 Subject: [PATCH 04/31] DEC-1373 flag button on posts/comments (#164) --- .../src/QueryApi.Examples.Feed.Comment.jsx | 22 +++++++++ .../feed/src/QueryApi.Examples.Feed.Post.jsx | 20 ++++++-- .../feed/src/QueryApi.Examples.Feed.Posts.jsx | 46 ++++++++++++++++--- 3 files changed, 79 insertions(+), 9 deletions(-) diff --git a/frontend/widgets/examples/feed/src/QueryApi.Examples.Feed.Comment.jsx b/frontend/widgets/examples/feed/src/QueryApi.Examples.Feed.Comment.jsx index 937197f75..14100679f 100644 --- a/frontend/widgets/examples/feed/src/QueryApi.Examples.Feed.Comment.jsx +++ b/frontend/widgets/examples/feed/src/QueryApi.Examples.Feed.Comment.jsx @@ -6,6 +6,7 @@ const blockHeight = props.blockHeight === "now" ? "now" : parseInt(props.blockHeight); State.init({ + hasBeenFlagged: false, content: JSON.parse(props.content) ?? undefined, notifyAccountId: undefined, }); @@ -122,6 +123,14 @@ const Actions = styled.div` margin: -6px -6px 6px; `; +if (state.hasBeenFlagged) { + return ( +
+ This content has been flagged for moderation +
+ ); +} + return (
@@ -196,6 +205,19 @@ return ( url: commentUrl, }} /> + { + State.update({ hasBeenFlagged: true }); + }, + }} + /> )} diff --git a/frontend/widgets/examples/feed/src/QueryApi.Examples.Feed.Post.jsx b/frontend/widgets/examples/feed/src/QueryApi.Examples.Feed.Post.jsx index 4e7673a31..ee3f45159 100644 --- a/frontend/widgets/examples/feed/src/QueryApi.Examples.Feed.Post.jsx +++ b/frontend/widgets/examples/feed/src/QueryApi.Examples.Feed.Post.jsx @@ -7,8 +7,8 @@ const blockHeight = // const subscribe = !!props.subscribe; const notifyAccountId = accountId; const postUrl = `https://alpha.near.org/#/${APP_OWNER}/widget/QueryApi.Examples.Feed.PostPage?accountId=${accountId}&blockHeight=${blockHeight}`; - State.init({ + hasBeenFlagged: false, postExists: true, comments: props.comments ?? undefined, content: JSON.parse(props.content) ?? undefined, @@ -185,7 +185,13 @@ const renderComment = (a) => { ); }; - +if (state.hasBeenFlagged) { + return ( +
+ This content has been flagged for moderation +
+ ); +} const renderedComments = state.comments.map(renderComment); return ( @@ -263,9 +269,17 @@ return ( url: postUrl, }} /> + { + State.update({ hasBeenFlagged: true }); + }, + }} + /> )} - {state.showReply && (
@@ -504,17 +480,13 @@ return (

{`${state.indexers[0].accountId}/${state.indexers[0].indexerName}`}

))} @@ -528,16 +500,12 @@ return (

{`${state.indexers[0].accountId}/${state.indexers[0].indexerName}`}

))} diff --git a/frontend/widgets/src/QueryApi.Editor.jsx b/frontend/widgets/src/QueryApi.Editor.jsx index 147080a90..8deadca58 100644 --- a/frontend/widgets/src/QueryApi.Editor.jsx +++ b/frontend/widgets/src/QueryApi.Editor.jsx @@ -1,12 +1,7 @@ const path = props.path || "query-api-editor"; const tab = props.tab || ""; -const REGISTRY_CONTRACT_ID = - props.REGISTRY_CONTRACT_ID || "queryapi.dataplatform.near"; let accountId = props.accountId || context.accountId; -let externalAppUrl = - props.EXTERNAL_APP_URL || "https://queryapi-frontend-24ktefolwq-ew.a.run.app"; -externalAppUrl += `/${path}?accountId=${accountId}`; -// let externalAppUrl = `http://localhost:3000/${path}?accountId=${accountId}`; +let externalAppUrl = `${REPL_EXTERNAL_APP_URL}/${path}?accountId=${accountId}`; if (props.indexerName) { externalAppUrl += `&indexerName=${props.indexerName}`; @@ -30,7 +25,7 @@ const registerFunctionHandler = (request, response) => { const jsonFilter = `{"indexer_rule_kind":"Action","matching_rule":{"rule":"ACTION_ANY","affected_account_id":"${contractFilter || "social.near"}","status":"SUCCESS"}}` Near.call( - REGISTRY_CONTRACT_ID, + `${REPL_REGISTRY_CONTRACT_ID}`, "register_indexer_function", { function_name: indexerName, @@ -47,7 +42,7 @@ let deleteIndexer = (request) => { const { indexerName } = request.payload; const gas = 200000000000000; Near.call( - REGISTRY_CONTRACT_ID, + `${REPL_REGISTRY_CONTRACT_ID}`, "remove_indexer_function", { function_name: indexerName, @@ -55,6 +50,7 @@ let deleteIndexer = (request) => { gas ); }; + /** * Request Handlers here */ diff --git a/frontend/widgets/src/QueryApi.IndexerCard.jsx b/frontend/widgets/src/QueryApi.IndexerCard.jsx index 431973dcb..f64545ba3 100644 --- a/frontend/widgets/src/QueryApi.IndexerCard.jsx +++ b/frontend/widgets/src/QueryApi.IndexerCard.jsx @@ -1,14 +1,8 @@ const accountId = props.accountId || context.accountId; const indexerName = props.indexerName; -const GRAPHQL_ENDPOINT = - props.GRAPHQL_ENDPOINT || - "https://queryapi-hasura-graphql-24ktefolwq-ew.a.run.app"; -const APP_OWNER = props.APP_OWNER || "dataplatform.near"; -const appPath = props.appPath || "App"; -const editUrl = `https://near.org/#/${APP_OWNER}/widget/QueryApi.${appPath}?selectedIndexerPath=${accountId}/${indexerName}&view=editor-window`; -const statusUrl = `https://near.org/#/${APP_OWNER}/widget/QueryApi.${appPath}?selectedIndexerPath=${accountId}/${indexerName}&view=indexer-status`; -// const playgroundLink = `https://near.org/#/${APP_OWNER}/widget/QueryApi.App?selectedIndexerPath=${accountId}/${indexerName}&view=editor-window&tab=playground`; -const playgroundLink = `https://cloud.hasura.io/public/graphiql?endpoint=${GRAPHQL_ENDPOINT}/v1/graphql&header=x-hasura-role%3A${accountId.replaceAll( +const editUrl = `https://near.org/#/${REPL_ACCOUNT_ID}/widget/QueryApi.App?selectedIndexerPath=${accountId}/${indexerName}&view=editor-window`; +const statusUrl = `https://near.org/#/${REPL_ACCOUNT_ID}/widget/QueryApi.App?selectedIndexerPath=${accountId}/${indexerName}&view=indexer-status`; +const playgroundLink = `https://cloud.hasura.io/public/graphiql?endpoint=${REPL_GRAPHQL_ENDPOINT}/v1/graphql&header=x-hasura-role%3A${accountId.replaceAll( ".", "_" )}`; diff --git a/frontend/widgets/src/QueryApi.IndexerExplorer.jsx b/frontend/widgets/src/QueryApi.IndexerExplorer.jsx index 62afe4def..8ac6e079e 100644 --- a/frontend/widgets/src/QueryApi.IndexerExplorer.jsx +++ b/frontend/widgets/src/QueryApi.IndexerExplorer.jsx @@ -1,10 +1,4 @@ const limitPerPage = 5; -const REGISTRY_CONTRACT_ID = - props.REGISTRY_CONTRACT_ID || "queryapi.dataplatform.near"; -let APP_OWNER = props.APP_OWNER || "dev-queryapi.dataplatform.near"; -const GRAPHQL_ENDPOINT = - props.GRAPHQL_ENDPOINT || - "https://queryapi-hasura-graphql-24ktefolwq-ew.a.run.app"; let totalIndexers = 0; const accountId = context.accountId; State.init({ @@ -21,7 +15,7 @@ if (props.tab && props.tab !== state.selectedTab) { }); } -Near.asyncView(REGISTRY_CONTRACT_ID, "list_indexer_functions").then((data) => { +Near.asyncView(`${REPL_REGISTRY_CONTRACT_ID}`, "list_indexer_functions").then((data) => { const indexers = []; const total_indexers = 0; Object.keys(data.All).forEach((accountId) => { @@ -242,13 +236,10 @@ return ( {state.all_indexers.map((indexer, i) => ( @@ -275,13 +266,10 @@ return ( {state.my_indexers.map((indexer, i) => ( diff --git a/frontend/widgets/src/QueryApi.IndexerStatus.jsx b/frontend/widgets/src/QueryApi.IndexerStatus.jsx index 492f650e2..4def76129 100644 --- a/frontend/widgets/src/QueryApi.IndexerStatus.jsx +++ b/frontend/widgets/src/QueryApi.IndexerStatus.jsx @@ -1,11 +1,9 @@ //props indexer_name const indexer_name = props.indexer_name; -const GRAPHQL_ENDPOINT = - props.GRAPHQL_ENDPOINT || - "https://queryapi-hasura-graphql-24ktefolwq-ew.a.run.app"; const LIMIT = 20; const accountId = props.accountId || context.accountId; + const H2 = styled.h2` font-size: 19px; line-height: 22px; @@ -110,7 +108,7 @@ State.init({ }); function fetchGraphQL(operationsDoc, operationName, variables) { - return asyncFetch(`${GRAPHQL_ENDPOINT}/v1/graphql`, { + return asyncFetch(`${REPL_GRAPHQL_ENDPOINT}/v1/graphql`, { method: "POST", body: JSON.stringify({ query: operationsDoc, @@ -121,7 +119,7 @@ function fetchGraphQL(operationsDoc, operationName, variables) { } const createGraphQLLink = () => { - const queryLink = `https://cloud.hasura.io/public/graphiql?endpoint=${GRAPHQL_ENDPOINT}/v1/graphql&query=query+IndexerQuery+%7B%0A++indexer_state%28where%3A+%7Bfunction_name%3A+%7B_eq%3A+%22function_placeholder%22%7D%7D%29+%7B%0A++++function_name%0A++++current_block_height%0A++%7D%0A++indexer_log_entries%28%0A++++where%3A+%7Bfunction_name%3A+%7B_eq%3A+%22function_placeholder%22%7D%7D%0A++++order_by%3A+%7B+timestamp%3A+desc%7D%0A++%29+%7B%0A++++function_name%0A++++id%0A++++message%0A++++timestamp%0A++%7D%0A%7D%0A`; + const queryLink = `https://cloud.hasura.io/public/graphiql?endpoint=${REPL_GRAPHQL_ENDPOINT}/v1/graphql&query=query+IndexerQuery+%7B%0A++indexer_state%28where%3A+%7Bfunction_name%3A+%7B_eq%3A+%22function_placeholder%22%7D%7D%29+%7B%0A++++function_name%0A++++current_block_height%0A++%7D%0A++indexer_log_entries%28%0A++++where%3A+%7Bfunction_name%3A+%7B_eq%3A+%22function_placeholder%22%7D%7D%0A++++order_by%3A+%7B+timestamp%3A+desc%7D%0A++%29+%7B%0A++++function_name%0A++++id%0A++++message%0A++++timestamp%0A++%7D%0A%7D%0A`; return queryLink.replaceAll( "function_placeholder", `${accountId}/${indexer_name}` diff --git a/frontend/widgets/src/QueryApi.dev-App.jsx b/frontend/widgets/src/QueryApi.dev-App.jsx deleted file mode 100644 index 224ab9d0d..000000000 --- a/frontend/widgets/src/QueryApi.dev-App.jsx +++ /dev/null @@ -1,25 +0,0 @@ -const GRAPHQL_ENDPOINT = "https://queryapi-hasura-graphql-vcqilefdcq-ew.a.run.app"; -const APP_OWNER = "dev-queryapi.dataplatform.near"; -const EXTERNAL_APP_URL = "https://queryapi-frontend-vcqilefdcq-ew.a.run.app"; -const REGISTRY_CONTRACT_ID = "dev-queryapi.dataplatform.near"; -const view = props.view; -const path = props.path; -const tab = props.tab; -const selectedIndexerPath = props.selectedIndexerPath; - -return ( - -); diff --git a/frontend/widgets/src/QueryApi.dev-App.metadata.json b/frontend/widgets/src/QueryApi.dev-App.metadata.json deleted file mode 100644 index bfbdf4a96..000000000 --- a/frontend/widgets/src/QueryApi.dev-App.metadata.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "description": "Entrypoint to Near QueryAPI's development widget which allows you to seamlessly create, manage, and discover new indexers", - "image": { - }, - "name": "QueryAPI Development", - "tags": { - "development": "" - } -} From cfb7f4680400a7f3087b8d9a1eabcd53b0b0941e Mon Sep 17 00:00:00 2001 From: Morgan McCauley Date: Fri, 18 Aug 2023 07:32:15 +1200 Subject: [PATCH 15/31] chore: Remove unused transaction cache (#182) --- indexer/queryapi_coordinator/src/cache.rs | 278 ---------------------- indexer/queryapi_coordinator/src/main.rs | 4 - 2 files changed, 282 deletions(-) delete mode 100644 indexer/queryapi_coordinator/src/cache.rs diff --git a/indexer/queryapi_coordinator/src/cache.rs b/indexer/queryapi_coordinator/src/cache.rs deleted file mode 100644 index fb7501801..000000000 --- a/indexer/queryapi_coordinator/src/cache.rs +++ /dev/null @@ -1,278 +0,0 @@ -use std::str::FromStr; - -use borsh::{BorshDeserialize, BorshSerialize}; -use cached::Cached; -use futures::future::try_join_all; -use serde::{Deserialize, Serialize}; - -use near_jsonrpc_client::errors::JsonRpcError; -use near_jsonrpc_primitives::types::query::RpcQueryError; -use near_lake_framework::near_indexer_primitives::{ - types, - views::{self, ExecutionStatusView}, - CryptoHash, IndexerExecutionOutcomeWithReceipt, IndexerTransactionWithOutcome, -}; - -#[derive(Serialize, Deserialize, BorshSerialize, BorshDeserialize, Debug)] -pub(crate) struct CacheValue { - pub transaction_hash: String, - pub parent_receipt_id: Option, - pub children_receipt_ids: Vec, -} - -pub(crate) async fn update_all( - streamer_message: &near_lake_framework::near_indexer_primitives::StreamerMessage, - redis_connection_manager: &storage::ConnectionManager, -) -> anyhow::Result<()> { - cache_txs_and_receipts(streamer_message, redis_connection_manager).await?; - Ok(()) -} - -pub(crate) async fn cache_txs_and_receipts( - streamer_message: &near_lake_framework::near_indexer_primitives::StreamerMessage, - redis_connection_manager: &storage::ConnectionManager, -) -> anyhow::Result<()> { - let cache_tx_receipts_future = streamer_message - .shards - .iter() - .filter_map(|shard| shard.chunk.as_ref()) - .map(|chunk| cache_receipts_from_tx(chunk.transactions.as_ref(), redis_connection_manager)); - - try_join_all(cache_tx_receipts_future).await?; - - let receipt_execution_outcomes: Vec = streamer_message - .shards - .iter() - .flat_map(|shard| shard.receipt_execution_outcomes.clone()) - .collect(); - - cache_receipts_from_outcomes(&receipt_execution_outcomes, redis_connection_manager).await?; - - Ok(()) -} - -async fn cache_receipts_from_tx( - transactions: &[IndexerTransactionWithOutcome], - redis_connection_manager: &storage::ConnectionManager, -) -> anyhow::Result<()> { - let push_receipt_to_watching_list_future = transactions.iter().map(|tx| async { - let transaction_hash_string = tx.transaction.hash.to_string(); - let converted_into_receipt_id = tx - .outcome - .execution_outcome - .outcome - .receipt_ids - .first() - .expect("`receipt_ids` must contain one Receipt ID") - .to_string(); - - let cache_value = CacheValue { - transaction_hash: transaction_hash_string, - parent_receipt_id: None, - children_receipt_ids: vec![], - }; - - storage::push_receipt_to_watching_list( - redis_connection_manager, - &converted_into_receipt_id, - &cache_value.try_to_vec().unwrap(), - ) - .await - }); - try_join_all(push_receipt_to_watching_list_future).await?; - - Ok(()) -} - -async fn cache_receipts_from_outcomes( - receipt_execution_outcomes: &[IndexerExecutionOutcomeWithReceipt], - redis_connection_manager: &storage::ConnectionManager, -) -> anyhow::Result<()> { - let cache_futures = receipt_execution_outcomes - .iter() - .map(|receipt_execution_outcome| { - cache_receipts_from_execution_outcome( - receipt_execution_outcome, - redis_connection_manager, - ) - }); - - try_join_all(cache_futures).await?; - Ok(()) -} - -async fn cache_receipts_from_execution_outcome( - receipt_execution_outcome: &IndexerExecutionOutcomeWithReceipt, - redis_connection_manager: &storage::ConnectionManager, -) -> anyhow::Result<()> { - let receipt_id = &receipt_execution_outcome.receipt.receipt_id.to_string(); - if let Ok(Some(cache_value_bytes)) = - storage::get::>>(redis_connection_manager, &receipt_id).await - { - // Add the newly produced receipt_ids to the watching list - let mut children_receipt_ids: Vec = receipt_execution_outcome - .execution_outcome - .outcome - .receipt_ids - .iter() - .map(ToString::to_string) - .collect(); - - // Add the success receipt to the watching list - if let ExecutionStatusView::SuccessReceiptId(receipt_id) = - receipt_execution_outcome.execution_outcome.outcome.status - { - children_receipt_ids.push(receipt_id.to_string()); - } - - if !children_receipt_ids.is_empty() { - // Rewrite CacheValue - let mut cache_value = CacheValue::try_from_slice(&cache_value_bytes)?; - cache_value.children_receipt_ids = children_receipt_ids.clone(); - storage::push_receipt_to_watching_list( - redis_connection_manager, - receipt_id, - &cache_value.try_to_vec().unwrap(), - ) - .await?; - - let push_receipt_to_watching_list_future = - children_receipt_ids.iter().map(|receipt_id_string| async { - let cache_value_bytes = CacheValue { - parent_receipt_id: Some(receipt_id.to_string()), - transaction_hash: cache_value.transaction_hash.clone(), - children_receipt_ids: vec![], - } - .try_to_vec() - .expect("Failed to BorshSerialize CacheValue"); - storage::push_receipt_to_watching_list( - redis_connection_manager, - receipt_id_string, - &cache_value_bytes, - ) - .await - }); - try_join_all(push_receipt_to_watching_list_future).await?; - } - } - Ok(()) -} - -pub(crate) async fn get_balance_retriable( - account_id: &types::AccountId, - block_hash: &str, - balance_cache: &crate::BalanceCache, - json_rpc_client: &near_jsonrpc_client::JsonRpcClient, -) -> anyhow::Result { - let mut interval = crate::INTERVAL; - let mut retry_attempt = 0usize; - - loop { - if retry_attempt == crate::RETRY_COUNT { - anyhow::bail!( - "Failed to perform query to RPC after {} attempts. Stop trying.\nAccount {}, block_hash {}", - crate::RETRY_COUNT, - account_id.to_string(), - block_hash.to_string() - ); - } - retry_attempt += 1; - - match get_balance(account_id, block_hash, balance_cache, json_rpc_client).await { - Ok(res) => return Ok(res), - Err(err) => { - tracing::error!( - target: crate::INDEXER, - "Failed to request account view details from RPC for account {}, block_hash {}.{}\n Retrying in {} milliseconds...", - account_id.to_string(), - block_hash.to_string(), - err, - interval.as_millis(), - ); - tokio::time::sleep(interval).await; - if interval < crate::MAX_DELAY_TIME { - interval *= 2; - } - } - } - } -} - -async fn get_balance( - account_id: &types::AccountId, - block_hash: &str, - balance_cache: &crate::BalanceCache, - json_rpc_client: &near_jsonrpc_client::JsonRpcClient, -) -> anyhow::Result { - let mut balances_cache_lock = balance_cache.lock().await; - let result = match balances_cache_lock.cache_get(account_id) { - None => { - let account_balance = - match get_account_view(json_rpc_client, account_id, block_hash).await { - Ok(account_view) => Ok(crate::BalanceDetails { - non_staked: account_view.amount, - staked: account_view.locked, - }), - Err(err) => match err.handler_error() { - Some(RpcQueryError::UnknownAccount { .. }) => Ok(crate::BalanceDetails { - non_staked: 0, - staked: 0, - }), - _ => Err(err.into()), - }, - }; - if let Ok(balance) = account_balance { - balances_cache_lock.cache_set(account_id.clone(), balance); - } - account_balance - } - Some(balance) => Ok(*balance), - }; - drop(balances_cache_lock); - result -} - -pub(crate) async fn save_latest_balance( - account_id: types::AccountId, - balance: &crate::BalanceDetails, - balance_cache: &crate::BalanceCache, -) { - let mut balances_cache_lock = balance_cache.lock().await; - balances_cache_lock.cache_set( - account_id, - crate::BalanceDetails { - non_staked: balance.non_staked, - staked: balance.staked, - }, - ); - drop(balances_cache_lock); -} - -async fn get_account_view( - json_rpc_client: &near_jsonrpc_client::JsonRpcClient, - account_id: &types::AccountId, - block_hash: &str, -) -> Result> { - let query = near_jsonrpc_client::methods::query::RpcQueryRequest { - block_reference: types::BlockReference::BlockId(types::BlockId::Hash( - CryptoHash::from_str(block_hash).unwrap(), - )), - request: views::QueryRequest::ViewAccount { - account_id: account_id.clone(), - }, - }; - - let account_response = json_rpc_client.call(query).await?; - match account_response.kind { - near_jsonrpc_primitives::types::query::QueryResponseKind::ViewAccount(account) => { - Ok(account) - } - _ => unreachable!( - "Unreachable code! Asked for ViewAccount (block_hash {}, account_id {})\nReceived\n\ - {:#?}\nReport this to https://github.com/near/near-jsonrpc-client-rs", - block_hash.to_string(), - account_id.to_string(), - account_response.kind - ), - } -} diff --git a/indexer/queryapi_coordinator/src/main.rs b/indexer/queryapi_coordinator/src/main.rs index 770852e2a..d6c5e0aab 100644 --- a/indexer/queryapi_coordinator/src/main.rs +++ b/indexer/queryapi_coordinator/src/main.rs @@ -12,7 +12,6 @@ use indexer_types::{IndexerQueueMessage, IndexerRegistry}; use opts::{Opts, Parser}; use storage::{self, ConnectionManager}; -pub(crate) mod cache; mod historical_block_processing; mod indexer_reducer; mod indexer_registry; @@ -130,9 +129,6 @@ async fn handle_streamer_message( context: QueryApiContext<'_>, indexer_registry: SharedIndexerRegistry, ) -> anyhow::Result { - // build context for enriching filter matches - cache::update_all(&context.streamer_message, context.redis_connection_manager).await?; - let mut indexer_registry_locked = indexer_registry.lock().await; let indexer_functions = indexer_registry::registry_as_vec_of_indexer_functions(&indexer_registry_locked); From 639064d5b8ffe3770b7903ec21ef34004ffcba62 Mon Sep 17 00:00:00 2001 From: Morgan McCauley Date: Fri, 18 Aug 2023 08:00:00 +1200 Subject: [PATCH 16/31] DPLT-1077 Process historical messages via Redis Streams (#181) --- .../src/historical_block_processing.rs | 50 ++++- ...ical_block_processing_integration_tests.rs | 4 + .../src/indexer_registry.rs | 1 + indexer/queryapi_coordinator/src/main.rs | 8 +- indexer/storage/src/lib.rs | 26 +-- runner/src/index.ts | 174 ++++-------------- runner/src/metrics.ts | 4 +- runner/src/redis-client/index.ts | 1 + runner/src/redis-client/redis-client.test.ts | 84 +++++++++ runner/src/redis-client/redis-client.ts | 86 +++++++++ 10 files changed, 275 insertions(+), 163 deletions(-) create mode 100644 runner/src/redis-client/index.ts create mode 100644 runner/src/redis-client/redis-client.test.ts create mode 100644 runner/src/redis-client/redis-client.ts diff --git a/indexer/queryapi_coordinator/src/historical_block_processing.rs b/indexer/queryapi_coordinator/src/historical_block_processing.rs index 366fe7da8..99bee3807 100644 --- a/indexer/queryapi_coordinator/src/historical_block_processing.rs +++ b/indexer/queryapi_coordinator/src/historical_block_processing.rs @@ -25,14 +25,20 @@ pub const MAX_RPC_BLOCKS_TO_PROCESS: u8 = 20; pub fn spawn_historical_message_thread( block_height: BlockHeight, new_indexer_function: &IndexerFunction, + redis_connection_manager: &storage::ConnectionManager, ) -> Option> { + let redis_connection_manager = redis_connection_manager.clone(); new_indexer_function.start_block_height.map(|_| { let new_indexer_function_copy = new_indexer_function.clone(); - tokio::spawn(process_historical_messages_or_handle_error( - block_height, - new_indexer_function_copy, - Opts::parse(), - )) + tokio::spawn(async move { + process_historical_messages_or_handle_error( + block_height, + new_indexer_function_copy, + Opts::parse(), + &redis_connection_manager, + ) + .await + }) }) } @@ -40,8 +46,16 @@ pub(crate) async fn process_historical_messages_or_handle_error( block_height: BlockHeight, indexer_function: IndexerFunction, opts: Opts, + redis_connection_manager: &storage::ConnectionManager, ) -> i64 { - match process_historical_messages(block_height, indexer_function, opts).await { + match process_historical_messages( + block_height, + indexer_function, + opts, + redis_connection_manager, + ) + .await + { Ok(block_difference) => block_difference, Err(err) => { // todo: when Coordinator can send log messages to Runner, send this error to Runner @@ -58,6 +72,7 @@ pub(crate) async fn process_historical_messages( block_height: BlockHeight, indexer_function: IndexerFunction, opts: Opts, + redis_connection_manager: &storage::ConnectionManager, ) -> anyhow::Result { let start_block = indexer_function.start_block_height.unwrap(); let block_difference: i64 = (block_height - start_block) as i64; @@ -124,7 +139,30 @@ pub(crate) async fn process_historical_messages( blocks_from_index.append(&mut blocks_between_indexed_and_current_block); let first_block_in_index = *blocks_from_index.first().unwrap_or(&start_block); + + if !blocks_from_index.is_empty() { + storage::sadd( + redis_connection_manager, + storage::STREAMS_SET_KEY, + storage::generate_historical_stream_key(&indexer_function.get_full_name()), + ) + .await?; + storage::set( + redis_connection_manager, + storage::generate_historical_storage_key(&indexer_function.get_full_name()), + serde_json::to_string(&indexer_function)?, + ) + .await?; + } + for current_block in blocks_from_index { + storage::xadd( + redis_connection_manager, + storage::generate_historical_stream_key(&indexer_function.get_full_name()), + &[("block_height", current_block)], + ) + .await?; + send_execution_message( block_height, first_block_in_index, diff --git a/indexer/queryapi_coordinator/src/historical_block_processing_integration_tests.rs b/indexer/queryapi_coordinator/src/historical_block_processing_integration_tests.rs index c2ca90a3a..e3d8428e4 100644 --- a/indexer/queryapi_coordinator/src/historical_block_processing_integration_tests.rs +++ b/indexer/queryapi_coordinator/src/historical_block_processing_integration_tests.rs @@ -76,6 +76,9 @@ mod tests { let opts = Opts::test_opts_with_aws(); let aws_config: &SdkConfig = &opts.lake_aws_sdk_config(); + let redis_connection_manager = storage::connect(&opts.redis_connection_string) + .await + .unwrap(); let fake_block_height = historical_block_processing::last_indexed_block_from_metadata(aws_config) .await @@ -84,6 +87,7 @@ mod tests { fake_block_height + 1, indexer_function, opts, + &redis_connection_manager, ) .await; assert!(result.unwrap() > 0); diff --git a/indexer/queryapi_coordinator/src/indexer_registry.rs b/indexer/queryapi_coordinator/src/indexer_registry.rs index 3ab2ec060..2c79cc70c 100644 --- a/indexer/queryapi_coordinator/src/indexer_registry.rs +++ b/indexer/queryapi_coordinator/src/indexer_registry.rs @@ -172,6 +172,7 @@ fn index_and_process_register_calls( crate::historical_block_processing::spawn_historical_message_thread( block_height, &mut new_indexer_function, + context.redis_connection_manager, ) { spawned_start_from_block_threads.push(thread); diff --git a/indexer/queryapi_coordinator/src/main.rs b/indexer/queryapi_coordinator/src/main.rs index d6c5e0aab..28c2dc3f3 100644 --- a/indexer/queryapi_coordinator/src/main.rs +++ b/indexer/queryapi_coordinator/src/main.rs @@ -198,19 +198,19 @@ async fn handle_streamer_message( storage::sadd( context.redis_connection_manager, - storage::INDEXER_SET_KEY, - indexer_function.get_full_name(), + storage::STREAMS_SET_KEY, + storage::generate_real_time_stream_key(&indexer_function.get_full_name()), ) .await?; storage::set( context.redis_connection_manager, - storage::generate_storage_key(&indexer_function.get_full_name()), + storage::generate_real_time_storage_key(&indexer_function.get_full_name()), serde_json::to_string(indexer_function)?, ) .await?; storage::xadd( context.redis_connection_manager, - storage::generate_stream_key(&indexer_function.get_full_name()), + storage::generate_real_time_stream_key(&indexer_function.get_full_name()), &[("block_height", block_height)], ) .await?; diff --git a/indexer/storage/src/lib.rs b/indexer/storage/src/lib.rs index 3753e186f..6c449b6b2 100644 --- a/indexer/storage/src/lib.rs +++ b/indexer/storage/src/lib.rs @@ -2,18 +2,26 @@ pub use redis::{self, aio::ConnectionManager, FromRedisValue, ToRedisArgs}; const STORAGE: &str = "storage_alertexer"; -pub const INDEXER_SET_KEY: &str = "indexers"; +pub const STREAMS_SET_KEY: &str = "streams"; pub async fn get_redis_client(redis_connection_str: &str) -> redis::Client { redis::Client::open(redis_connection_str).expect("can create redis client") } -pub fn generate_storage_key(name: &str) -> String { - format!("{}:storage", name) +pub fn generate_real_time_stream_key(prefix: &str) -> String { + format!("{}:real_time:stream", prefix) } -pub fn generate_stream_key(name: &str) -> String { - format!("{}:stream", name) +pub fn generate_real_time_storage_key(prefix: &str) -> String { + format!("{}:real_time:stream:storage", prefix) +} + +pub fn generate_historical_stream_key(prefix: &str) -> String { + format!("{}:historical:stream", prefix) +} + +pub fn generate_historical_storage_key(prefix: &str) -> String { + format!("{}:historical:stream:storage", prefix) } pub async fn connect(redis_connection_str: &str) -> anyhow::Result { @@ -84,14 +92,6 @@ pub async fn xadd( ) -> anyhow::Result<()> { tracing::debug!(target: STORAGE, "XADD: {:?}, {:?}", stream_key, fields); - // TODO: Remove stream cap when we finally start processing it - redis::cmd("XTRIM") - .arg(&stream_key) - .arg("MAXLEN") - .arg(100) - .query_async(&mut redis_connection_manager.clone()) - .await?; - let mut cmd = redis::cmd("XADD"); cmd.arg(stream_key).arg("*"); diff --git a/runner/src/index.ts b/runner/src/index.ts index 66bbc10cb..b1b8fc00b 100644 --- a/runner/src/index.ts +++ b/runner/src/index.ts @@ -1,141 +1,32 @@ -import { createClient } from 'redis'; - import Indexer from './indexer'; import * as metrics from './metrics'; +import RedisClient from './redis-client'; -const client = createClient({ url: process.env.REDIS_CONNECTION_STRING }); const indexer = new Indexer('mainnet'); +const redisClient = new RedisClient(); metrics.startServer().catch((err) => { console.error('Failed to start metrics server', err); }); -// const BATCH_SIZE = 1; -const STREAM_SMALLEST_ID = '0'; -// const STREAM_THROTTLE_MS = 250; const STREAM_HANDLER_THROTTLE_MS = 500; -const INDEXER_SET_KEY = 'indexers'; - -client.on('error', (err) => { console.log('Redis Client Error', err); }); - -const generateStreamKey = (name: string): string => { - return `${name}:stream`; -}; - -const generateStorageKey = (name: string): string => { - return `${name}:storage`; -}; - -const generateStreamLastIdKey = (name: string): string => { - return `${name}:stream:lastId`; -}; - -const runFunction = async (indexerName: string, blockHeight: string): Promise => { - const { account_id: accountId, function_name: functionName, code, schema } = await getIndexerData( - indexerName, - ); - - const functions = { - [indexerName]: { - account_id: accountId, - function_name: functionName, - code, - schema, - provisioned: false, - }, - }; - - await indexer.runFunctions(Number(blockHeight), functions, false, { - provision: true, - }); -}; - -interface StreamMessage { - id: string - message: Message -} - -type StreamMessages = Array>; - -const getMessagesFromStream = async >( - indexerName: string, - lastId: string | null, - count: number, -): Promise | null> => { - const id = lastId ?? STREAM_SMALLEST_ID; - - const results = await client.xRead( - { key: generateStreamKey(indexerName), id }, - // can't use blocking calls as running single threaded - { COUNT: count } - ); - - return results?.[0].messages as StreamMessages; -}; - -const incrementStreamId = (id: string): string => { - const [timestamp, sequenceNumber] = id.split('-'); - const nextSequenceNumber = Number(sequenceNumber) + 1; - return `${timestamp}-${nextSequenceNumber}`; -}; - -const getUnprocessedMessages = async >( - indexerName: string, - startId: string | null -): Promise>> => { - const nextId = startId ? incrementStreamId(startId) : STREAM_SMALLEST_ID; +const processStream = async (streamKey: string): Promise => { + console.log('Started processing stream: ', streamKey); - const results = await client.xRange(generateStreamKey(indexerName), nextId, '+'); + let indexerName = ''; + let startTime = 0; + let streamType = ''; - return results as Array>; -}; - -const getLastProcessedId = async ( - indexerName: string, -): Promise => { - return await client.get(generateStreamLastIdKey(indexerName)); -}; - -const setLastProcessedId = async ( - indexerName: string, - lastId: string, -): Promise => { - await client.set(generateStreamLastIdKey(indexerName), lastId); -}; - -interface IndexerConfig { - account_id: string - function_name: string - code: string - schema: string -} - -const getIndexerData = async (indexerName: string): Promise => { - const results = await client.get(generateStorageKey(indexerName)); - - if (results === null) { - throw new Error(`${indexerName} does not have any data`); - } - - return JSON.parse(results); -}; - -type IndexerStreamMessage = { - block_height: string -} & Record; - -const processStream = async (indexerName: string): Promise => { while (true) { try { - const startTime = performance.now(); + startTime = performance.now(); + streamType = redisClient.getStreamType(streamKey); - const lastProcessedId = await getLastProcessedId(indexerName); - const messages = await getMessagesFromStream( - indexerName, - lastProcessedId, - 1, - ); + const messages = await redisClient.getNextStreamMessage(streamKey); + const indexerConfig = await redisClient.getStreamStorage(streamKey); + + indexerName = `${indexerConfig.account_id}/${indexerConfig.function_name}`; if (messages == null) { continue; @@ -143,20 +34,29 @@ const processStream = async (indexerName: string): Promise => { const [{ id, message }] = messages; - await runFunction(indexerName, message.block_height); - - await setLastProcessedId(indexerName, id); - - const endTime = performance.now(); + const functions = { + [indexerName]: { + account_id: indexerConfig.account_id, + function_name: indexerConfig.function_name, + code: indexerConfig.code, + schema: indexerConfig.schema, + provisioned: false, + }, + }; + await indexer.runFunctions(Number(message.block_height), functions, false, { + provision: true, + }); - metrics.EXECUTION_DURATION.labels({ indexer: indexerName }).set(endTime - startTime); + await redisClient.deleteStreamMessage(streamKey, id); - const unprocessedMessages = await getUnprocessedMessages(indexerName, lastProcessedId); - metrics.UNPROCESSED_STREAM_MESSAGES.labels({ indexer: indexerName }).set(unprocessedMessages?.length ?? 0); + const unprocessedMessages = await redisClient.getUnprocessedStreamMessages(streamKey); + metrics.UNPROCESSED_STREAM_MESSAGES.labels({ indexer: indexerName, type: streamType }).set(unprocessedMessages?.length ?? 0); console.log(`Success: ${indexerName}`); } catch (err) { console.log(`Failed: ${indexerName}`, err); + } finally { + metrics.EXECUTION_DURATION.labels({ indexer: indexerName, type: streamType }).set(performance.now() - startTime); } } }; @@ -165,20 +65,18 @@ type StreamHandlers = Record>; void (async function main () { try { - await client.connect(); - const streamHandlers: StreamHandlers = {}; while (true) { - const indexers = await client.sMembers(INDEXER_SET_KEY); + const streamKeys = await redisClient.getStreams(); - indexers.forEach((indexerName) => { - if (streamHandlers[indexerName] !== undefined) { + streamKeys.forEach((streamKey) => { + if (streamHandlers[streamKey] !== undefined) { return; } - const handler = processStream(indexerName); - streamHandlers[indexerName] = handler; + const handler = processStream(streamKey); + streamHandlers[streamKey] = handler; }); await new Promise((resolve) => @@ -186,6 +84,6 @@ void (async function main () { ); } } finally { - await client.disconnect(); + await redisClient.disconnect(); } })(); diff --git a/runner/src/metrics.ts b/runner/src/metrics.ts index ff4ffe0d4..f4fbeb801 100644 --- a/runner/src/metrics.ts +++ b/runner/src/metrics.ts @@ -4,13 +4,13 @@ import promClient from 'prom-client'; export const UNPROCESSED_STREAM_MESSAGES = new promClient.Gauge({ name: 'queryapi_runner_unprocessed_stream_messages', help: 'Number of Redis Stream messages not yet processed', - labelNames: ['indexer'], + labelNames: ['indexer', 'type'], }); export const EXECUTION_DURATION = new promClient.Gauge({ name: 'queryapi_runner_execution_duration_milliseconds', help: 'Time taken to execute an indexer function', - labelNames: ['indexer'], + labelNames: ['indexer', 'type'], }); export const startServer = async (): Promise => { diff --git a/runner/src/redis-client/index.ts b/runner/src/redis-client/index.ts new file mode 100644 index 000000000..efa0f96e7 --- /dev/null +++ b/runner/src/redis-client/index.ts @@ -0,0 +1 @@ +export { default } from './redis-client'; diff --git a/runner/src/redis-client/redis-client.test.ts b/runner/src/redis-client/redis-client.test.ts new file mode 100644 index 000000000..85588ccab --- /dev/null +++ b/runner/src/redis-client/redis-client.test.ts @@ -0,0 +1,84 @@ +import RedisClient from './redis-client'; + +describe('RedisClient', () => { + it('returns the first message', async () => { + const mockClient = { + on: jest.fn(), + connect: jest.fn().mockResolvedValue(null), + xRead: jest.fn().mockResolvedValue(null), + } as any; + + const client = new RedisClient(mockClient); + + const message = await client.getNextStreamMessage('streamKey'); + + expect(mockClient.xRead).toHaveBeenCalledWith( + { key: 'streamKey', id: '0' }, + { COUNT: 1 } + ); + expect(message).toBeUndefined(); + }); + + it('deletes the stream message', async () => { + const mockClient = { + on: jest.fn(), + connect: jest.fn().mockResolvedValue(null), + xDel: jest.fn().mockResolvedValue(null), + } as any; + + const client = new RedisClient(mockClient); + + await client.deleteStreamMessage('streamKey', '1-1'); + + expect(mockClient.xDel).toHaveBeenCalledWith('streamKey', '1-1'); + }); + + it('returns the range of messages after the passed id', async () => { + const mockClient = { + on: jest.fn(), + connect: jest.fn().mockResolvedValue(null), + xRange: jest.fn().mockResolvedValue([ + 'data' + ]), + } as any; + + const client = new RedisClient(mockClient); + + const unprocessedMessages = await client.getUnprocessedStreamMessages('streamKey'); + + expect(mockClient.xRange).toHaveBeenCalledWith('streamKey', '0', '+'); + expect(unprocessedMessages).toEqual([ + 'data' + ]); + }); + + it('returns stream storage data', async () => { + const mockClient = { + on: jest.fn(), + connect: jest.fn().mockResolvedValue(null), + get: jest.fn().mockResolvedValue(JSON.stringify({ account_id: '123', function_name: 'testFunc' })), + } as any; + + const client = new RedisClient(mockClient); + + const storageData = await client.getStreamStorage('streamKey'); + + expect(mockClient.get).toHaveBeenCalledWith('streamKey:storage'); + expect(storageData).toEqual({ account_id: '123', function_name: 'testFunc' }); + }); + + it('returns the list of streams', async () => { + const mockClient = { + on: jest.fn(), + connect: jest.fn().mockResolvedValue(null), + sMembers: jest.fn().mockResolvedValue(['streamKey1', 'streamKey2']), + } as any; + + const client = new RedisClient(mockClient); + + const streams = await client.getStreams(); + + expect(mockClient.sMembers).toHaveBeenCalledWith('streams'); + expect(streams).toEqual(['streamKey1', 'streamKey2']); + }); +}); diff --git a/runner/src/redis-client/redis-client.ts b/runner/src/redis-client/redis-client.ts new file mode 100644 index 000000000..9caa66226 --- /dev/null +++ b/runner/src/redis-client/redis-client.ts @@ -0,0 +1,86 @@ +import { createClient, type RedisClientType } from 'redis'; + +interface StreamMessage { + id: string + message: { + block_height: string + } +} + +interface StreamStorage { + account_id: string + function_name: string + code: string + schema: string +} + +type StreamType = 'historical' | 'real-time'; + +export default class RedisClient { + SMALLEST_STREAM_ID = '0'; + LARGEST_STREAM_ID = '+'; + STREAMS_SET_KEY = 'streams'; + + constructor ( + private readonly client: RedisClientType = createClient({ url: process.env.REDIS_CONNECTION_STRING }) + ) { + client.on('error', (err) => { console.log('Redis Client Error', err); }); + client.connect().catch(console.error); + } + + private generateStorageKey (streamkey: string): string { + return `${streamkey}:storage`; + }; + + getStreamType (streamKey: string): StreamType { + if (streamKey.endsWith(':historical:stream')) { + return 'historical'; + } + return 'real-time'; + } + + async disconnect (): Promise { + await this.client.disconnect(); + } + + async getNextStreamMessage ( + streamKey: string, + ): Promise { + const results = await this.client.xRead( + { key: streamKey, id: this.SMALLEST_STREAM_ID }, + { COUNT: 1 } + ); + + return results?.[0].messages as StreamMessage[]; + }; + + async deleteStreamMessage ( + streamKey: string, + id: string, + ): Promise { + await this.client.xDel(streamKey, id); + }; + + async getUnprocessedStreamMessages ( + streamKey: string, + ): Promise { + const results = await this.client.xRange(streamKey, this.SMALLEST_STREAM_ID, this.LARGEST_STREAM_ID); + + return results as StreamMessage[]; + }; + + async getStreamStorage (streamKey: string): Promise { + const storageKey = this.generateStorageKey(streamKey); + const results = await this.client.get(storageKey); + + if (results === null) { + throw new Error(`${storageKey} does not have any data`); + } + + return JSON.parse(results); + }; + + async getStreams (): Promise { + return await this.client.sMembers(this.STREAMS_SET_KEY); + } +} From 10aae59485ece18d859945ab1206856f2d22b7ff Mon Sep 17 00:00:00 2001 From: Darun Seethammagari Date: Tue, 22 Aug 2023 09:07:05 -0700 Subject: [PATCH 17/31] feat: Generate insert and select methods for context object under db (#177) Co-authored-by: Darun Seethammagari --- .gitignore | 1 + README.md | 2 + frontend/src/components/Editor/Editor.js | 7 +- frontend/src/utils/indexerRunner.js | 104 +++++++++--- runner/src/dml-handler/dml-handler.test.ts | 93 +++++++++++ runner/src/dml-handler/dml-handler.ts | 63 ++++++++ runner/src/dml-handler/index.ts | 1 + .../src/hasura-client/hasura-client.test.ts | 76 +++++++++ runner/src/hasura-client/hasura-client.ts | 9 ++ runner/src/indexer/indexer.test.ts | 153 ++++++++++++++++-- runner/src/indexer/indexer.ts | 56 ++++++- runner/src/provisioner/provisioner.ts | 31 ++-- runner/src/utility.ts | 13 ++ 13 files changed, 544 insertions(+), 65 deletions(-) create mode 100644 runner/src/dml-handler/dml-handler.test.ts create mode 100644 runner/src/dml-handler/dml-handler.ts create mode 100644 runner/src/dml-handler/index.ts create mode 100644 runner/src/utility.ts diff --git a/.gitignore b/.gitignore index 6440537ad..6a2af26a0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ redis/ *.log /indexer/blocks/ +node_modules/ diff --git a/README.md b/README.md index bb82abb01..7112dd31d 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ when they match new blocks by placing messages on an SQS queue. Spawns historica indexer_rules_engine, storage. 2. [Indexer Runner](.indexer-js-queue-handler) Retrieves messages from the SQS queue, fetches the matching block and executes the IndexerFunction. +3. [Runner](.runner) + Retrieves messages from Redis Stream, fetching matching block and executes the IndexerFunction. 3. [IndexerFunction Editor UI](./frontend) Serves the editor UI within the dashboard widget and mediates some communication with the GraphQL DB and block server. 4. [Hasura Authentication Service](./hasura-authentication-service) diff --git a/frontend/src/components/Editor/Editor.js b/frontend/src/components/Editor/Editor.js index 4d217e6b6..9745661f8 100644 --- a/frontend/src/components/Editor/Editor.js +++ b/frontend/src/components/Editor/Editor.js @@ -261,10 +261,11 @@ const Editor = ({ async function executeIndexerFunction(option = "latest", startingBlockHeight = null) { setIsExecutingIndexerFunction(() => true) + const schemaName = indexerDetails.accountId.concat("_", indexerDetails.indexerName).replace(/[^a-zA-Z0-9]/g, '_'); switch (option) { case "debugList": - await indexerRunner.executeIndexerFunctionOnHeights(heights, indexingCode, option) + await indexerRunner.executeIndexerFunctionOnHeights(heights, indexingCode, schema, schemaName, option) break case "specific": if (startingBlockHeight === null && Number(startingBlockHeight) === 0) { @@ -272,11 +273,11 @@ const Editor = ({ break } - await indexerRunner.start(startingBlockHeight, indexingCode, option) + await indexerRunner.start(startingBlockHeight, indexingCode, schema, schemaName, option) break case "latest": const latestHeight = await requestLatestBlockHeight() - if (latestHeight) await indexerRunner.start(latestHeight - 10, indexingCode, option) + if (latestHeight) await indexerRunner.start(latestHeight - 10, indexingCode, schema, schemaName, option) } setIsExecutingIndexerFunction(() => false) } diff --git a/frontend/src/utils/indexerRunner.js b/frontend/src/utils/indexerRunner.js index 123226bbb..6df114c29 100644 --- a/frontend/src/utils/indexerRunner.js +++ b/frontend/src/utils/indexerRunner.js @@ -10,7 +10,7 @@ export default class IndexerRunner { this.shouldStop = false; } - async start(startingHeight, indexingCode, option) { + async start(startingHeight, indexingCode, schema, schemaName, option) { this.currentHeight = startingHeight; this.shouldStop = false; console.clear() @@ -32,7 +32,7 @@ export default class IndexerRunner { this.stop() } if (blockDetails) { - await this.executeIndexerFunction(this.currentHeight, blockDetails, indexingCode); + await this.executeIndexerFunction(this.currentHeight, blockDetails, indexingCode, schema, schemaName); this.currentHeight++; await this.delay(1000); } @@ -50,8 +50,24 @@ export default class IndexerRunner { return new Promise((resolve) => setTimeout(resolve, ms)); } - async executeIndexerFunction(height, blockDetails, indexingCode) { + validateTableNames(tableNames) { + if (!(Array.isArray(tableNames) && tableNames.length > 0)) { + throw new Error("Schema does not have any tables. There should be at least one table."); + } + const correctTableNameFormat = /^[a-zA-Z_][a-zA-Z0-9_]*$/; + + tableNames.forEach(name => { + if (!correctTableNameFormat.test(name)) { + throw new Error(`Table name ${name} is not formatted correctly. Table names must not start with a number and only contain alphanumerics or underscores.`); + } + }); + } + + async executeIndexerFunction(height, blockDetails, indexingCode, schema, schemaName) { let innerCode = indexingCode.match(/getBlock\s*\([^)]*\)\s*{([\s\S]*)}/)[1]; + let tableNames = Array.from(schema.matchAll(/CREATE TABLE\s+"(\w+)"/g), match => match[1]); // Get first capturing group of each match + this.validateTableNames(tableNames); + if (blockDetails) { const block = Block.fromStreamerMessage(blockDetails); block.actions() @@ -59,11 +75,11 @@ export default class IndexerRunner { block.events() console.log(block) - await this.runFunction(blockDetails, height, innerCode); + await this.runFunction(blockDetails, height, innerCode, schemaName, tableNames); } } - async executeIndexerFunctionOnHeights(heights, indexingCode) { + async executeIndexerFunctionOnHeights(heights, indexingCode, schema, schemaName) { console.clear() console.group('%c Welcome! Lets test your indexing logic on some Near Blocks!', 'color: white; background-color: navy; padding: 5px;'); if (heights.length === 0) { @@ -80,14 +96,14 @@ export default class IndexerRunner { console.log(error) } console.time('Indexing Execution Complete') - this.executeIndexerFunction(height, blockDetails, indexingCode) + this.executeIndexerFunction(height, blockDetails, indexingCode, schema, schemaName) console.timeEnd('Indexing Execution Complete') console.groupEnd() } console.groupEnd() } - async runFunction(streamerMessage, blockHeight, indexerCode) { + async runFunction(streamerMessage, blockHeight, indexerCode, schemaName, tableNames) { const innerCodeWithBlockHelper = ` const block = Block.fromStreamerMessage(streamerMessage); @@ -125,21 +141,20 @@ export default class IndexerRunner { "", () => { let operationType, operationName - const match = query.match(/(query|mutation)\s+(\w+)\s*(\(.*?\))?\s*\{([\s\S]*)\}/); - if (match) { - operationType = match[1]; - operationName = match[2]; - } - - console.group(`Executing GraphQL ${operationType}: (${operationName})`); - if (operationType === 'mutation') console.log('%c Mutations in debug mode do not alter the database', 'color: black; background-color: yellow; padding: 5px;'); - console.group(`Data passed to ${operationType}`); - console.dir(mutationData); - console.groupEnd(); - console.group(`Data returned by ${operationType}`); - console.log({}) - console.groupEnd(); - console.groupEnd(); + const match = query.match(/(query|mutation)\s+(\w+)\s*(\(.*?\))?\s*\{([\s\S]*)\}/); + if (match) { + operationType = match[1]; + operationName = match[2]; + } + console.group(`Executing GraphQL ${operationType}: (${operationName})`); + if (operationType === 'mutation') console.log('%c Mutations in debug mode do not alter the database', 'color: black; background-color: yellow; padding: 5px;'); + console.group(`Data passed to ${operationType}`); + console.dir(mutationData); + console.groupEnd(); + console.group(`Data returned by ${operationType}`); + console.log({}) + console.groupEnd(); + console.groupEnd(); } ); return {}; @@ -147,11 +162,56 @@ export default class IndexerRunner { log: async (message) => { this.handleLog(blockHeight, message); }, + db: this.buildDatabaseContext(blockHeight, schemaName, tableNames) }; wrappedFunction(Block, streamerMessage, context); } + buildDatabaseContext (blockHeight, schemaName, tables) { + try { + const result = tables.reduce((prev, tableName) => ({ + ...prev, + [`insert_${tableName}`]: async (objects) => await this.insert(blockHeight, schemaName, tableName, objects), + [`select_${tableName}`]: async (object, limit = 0) => await this.select(blockHeight, schemaName, tableName, object, limit), + }), {}); + return result; + } catch (error) { + console.error('Caught error when generating DB methods. Falling back to generic methods.', error); + return { + insert: async (tableName, objects) => { + this.insert(blockHeight, schemaName, tableName, objects); + }, + select: async (tableName, object, limit = 0) => { + this.select(blockHeight, schemaName, tableName, object, limit); + } + }; + } + } + + insert(blockHeight, schemaName, tableName, objects) { + this.handleLog( + blockHeight, + "", + () => { + console.log('Inserting object %s into table %s on schema %s', JSON.stringify(objects), tableName, schemaName); + } + ); + return {}; + } + + select(blockHeight, schemaName, tableName, object, limit) { + this.handleLog( + blockHeight, + "", + () => { + const roundedLimit = Math.round(limit); + console.log('Selecting objects with values %s from table %s on schema %s with %s limit', JSON.stringify(object), tableName, schemaName, limit === 0 ? 'no' : roundedLimit.toString()); + } + ); + return {}; + } + // deprecated replaceNewLines(code) { return code.replace(/\\n/g, "\n").replace(/\\"/g, '"'); diff --git a/runner/src/dml-handler/dml-handler.test.ts b/runner/src/dml-handler/dml-handler.test.ts new file mode 100644 index 000000000..e0c92c057 --- /dev/null +++ b/runner/src/dml-handler/dml-handler.test.ts @@ -0,0 +1,93 @@ +import pgFormat from 'pg-format'; +import DmlHandler from './dml-handler'; + +describe('DML Handler tests', () => { + const hasuraClient: any = { + getDbConnectionParameters: jest.fn().mockReturnValue({ + database: 'test_near', + host: 'postgres', + password: 'test_pass', + port: 5432, + username: 'test_near' + }) + }; + let PgClient: any; + let query: any; + + const ACCOUNT = 'test_near'; + const SCHEMA = 'test_schema'; + const TABLE_NAME = 'test_table'; + + beforeEach(() => { + query = jest.fn().mockReturnValue({ rows: [] }); + PgClient = jest.fn().mockImplementation(() => { + return { query, format: pgFormat }; + }); + }); + + test('Test valid insert one with array', async () => { + const inputObj = { + account_id: 'test_acc_near', + block_height: 999, + block_timestamp: 'UTC', + content: 'test_content', + receipt_id: 111, + accounts_liked: JSON.stringify(['cwpuzzles.near', 'devbose.near']) + }; + + const dmlHandler = new DmlHandler(ACCOUNT, hasuraClient, PgClient); + + await dmlHandler.insert(SCHEMA, TABLE_NAME, [inputObj]); + expect(query.mock.calls).toEqual([ + ['INSERT INTO test_schema.test_table (account_id,block_height,block_timestamp,content,receipt_id,accounts_liked) VALUES (\'test_acc_near\', \'999\', \'UTC\', \'test_content\', \'111\', \'["cwpuzzles.near","devbose.near"]\') RETURNING *;', []] + ]); + }); + + test('Test valid insert multiple rows with array', async () => { + const inputObj = [{ + account_id: 'morgs_near', + block_height: 1, + receipt_id: 'abc', + }, + { + account_id: 'morgs_near', + block_height: 2, + receipt_id: 'abc', + }]; + + const dmlHandler = new DmlHandler(ACCOUNT, hasuraClient, PgClient); + + await dmlHandler.insert(SCHEMA, TABLE_NAME, [inputObj]); + expect(query.mock.calls).toEqual([ + ['INSERT INTO test_schema.test_table (0,1) VALUES (\'{"account_id":"morgs_near","block_height":1,"receipt_id":"abc"}\'::jsonb, \'{"account_id":"morgs_near","block_height":2,"receipt_id":"abc"}\'::jsonb) RETURNING *;', []] + ]); + }); + + test('Test valid select on two fields', async () => { + const inputObj = { + account_id: 'test_acc_near', + block_height: 999, + }; + + const dmlHandler = new DmlHandler(ACCOUNT, hasuraClient, PgClient); + + await dmlHandler.select(SCHEMA, TABLE_NAME, inputObj); + expect(query.mock.calls).toEqual([ + ['SELECT * FROM test_schema.test_table WHERE account_id=$1 AND block_height=$2', Object.values(inputObj)] + ]); + }); + + test('Test valid select on two fields with limit', async () => { + const inputObj = { + account_id: 'test_acc_near', + block_height: 999, + }; + + const dmlHandler = new DmlHandler(ACCOUNT, hasuraClient, PgClient); + + await dmlHandler.select(SCHEMA, TABLE_NAME, inputObj, 1); + expect(query.mock.calls).toEqual([ + ['SELECT * FROM test_schema.test_table WHERE account_id=$1 AND block_height=$2 LIMIT 1', Object.values(inputObj)] + ]); + }); +}); diff --git a/runner/src/dml-handler/dml-handler.ts b/runner/src/dml-handler/dml-handler.ts new file mode 100644 index 000000000..45dbdcb73 --- /dev/null +++ b/runner/src/dml-handler/dml-handler.ts @@ -0,0 +1,63 @@ +import { wrapError } from '../utility'; +import PgClientModule from '../pg-client'; +import HasuraClient from '../hasura-client/hasura-client'; + +export default class DmlHandler { + private pgClient!: PgClientModule; + private readonly initialized: Promise; + + constructor ( + private readonly account: string, + private readonly hasuraClient: HasuraClient = new HasuraClient(), + private readonly PgClient = PgClientModule, + ) { + this.initialized = this.initialize(); + } + + private async initialize (): Promise { + const connectionParameters = await this.hasuraClient.getDbConnectionParameters(this.account); + this.pgClient = new this.PgClient({ + user: connectionParameters.username, + password: connectionParameters.password, + host: process.env.PGHOST, + port: Number(connectionParameters.port), + database: connectionParameters.database, + }); + } + + async insert (schemaName: string, tableName: string, objects: any[]): Promise { + await this.initialized; // Ensure constructor completed before proceeding + if (!objects?.length) { + return []; + } + + const keys = Object.keys(objects[0]); + // Get array of values from each object, and return array of arrays as result. Expects all objects to have the same number of items in same order + const values = objects.map(obj => keys.map(key => obj[key])); + const query = `INSERT INTO ${schemaName}.${tableName} (${keys.join(',')}) VALUES %L RETURNING *;`; + + const result = await wrapError(async () => await this.pgClient.query(this.pgClient.format(query, values), []), `Failed to execute '${query}' on ${schemaName}.${tableName}.`); + if (result.rows?.length === 0) { + console.log('No rows were inserted.'); + } + return result.rows; + } + + async select (schemaName: string, tableName: string, object: any, limit: number | null = null): Promise { + await this.initialized; // Ensure constructor completed before proceeding + + const keys = Object.keys(object); + const values = Object.values(object); + const param = Array.from({ length: keys.length }, (_, index) => `${keys[index]}=$${index + 1}`).join(' AND '); + let query = `SELECT * FROM ${schemaName}.${tableName} WHERE ${param}`; + if (limit !== null) { + query = query.concat(' LIMIT ', Math.round(limit).toString()); + } + + const result = await wrapError(async () => await this.pgClient.query(this.pgClient.format(query), values), `Failed to execute '${query}' on ${schemaName}.${tableName}.`); + if (!(result.rows && result.rows.length > 0)) { + console.log('No rows were selected.'); + } + return result.rows; + } +} diff --git a/runner/src/dml-handler/index.ts b/runner/src/dml-handler/index.ts new file mode 100644 index 000000000..8beb5a70c --- /dev/null +++ b/runner/src/dml-handler/index.ts @@ -0,0 +1 @@ +export { default } from './dml-handler'; diff --git a/runner/src/hasura-client/hasura-client.test.ts b/runner/src/hasura-client/hasura-client.test.ts index 56a19cef2..d4b621886 100644 --- a/runner/src/hasura-client/hasura-client.test.ts +++ b/runner/src/hasura-client/hasura-client.test.ts @@ -244,4 +244,80 @@ describe('HasuraClient', () => { expect(mockFetch).toBeCalledTimes(1); // to fetch the foreign keys }); + + it('returns connection parameters for valid and invalid users', async () => { + const testUsers = { + testA_near: 'passA', + testB_near: 'passB', + testC_near: 'passC' + }; + const TEST_METADATA = generateMetadata(testUsers); + const mockFetch = jest + .fn() + .mockResolvedValue({ + status: 200, + text: () => JSON.stringify({ metadata: TEST_METADATA }) + }); + const client = new HasuraClient({ fetch: mockFetch as unknown as typeof fetch }); + const result = await client.getDbConnectionParameters('testB_near'); + expect(result).toEqual(generateConnectionParameter('testB_near', 'passB')); + await expect(client.getDbConnectionParameters('fake_near')).rejects.toThrow('Could not find connection parameters for user fake_near on respective database.'); + }); }); + +function generateMetadata (testUsers: any): any { + const sources = []; + // Insert default source which has different format than the rest + sources.push({ + name: 'default', + kind: 'postgres', + tables: [], + configuration: { + connection_info: { + database_url: { from_env: 'HASURA_GRAPHQL_DATABASE_URL' }, + isolation_level: 'read-committed', + pool_settings: { + connection_lifetime: 600, + idle_timeout: 180, + max_connections: 50, + retries: 1 + }, + use_prepared_statements: true + } + } + }); + + Object.keys(testUsers).forEach((user) => { + sources.push(generateSource(user, testUsers[user])); + }); + + return { + version: 3, + sources + }; +} + +function generateSource (user: string, password: string): any { + return { + name: user, + kind: 'postgres', + tables: [], + configuration: { + connection_info: { + database_url: { connection_parameters: generateConnectionParameter(user, password) }, + isolation_level: 'read-committed', + use_prepared_statements: false + } + } + }; +} + +function generateConnectionParameter (user: string, password: string): any { + return { + database: user, + host: 'postgres', + password, + port: 5432, + username: user + }; +} diff --git a/runner/src/hasura-client/hasura-client.ts b/runner/src/hasura-client/hasura-client.ts index 4edf6f1bb..0162c4b41 100644 --- a/runner/src/hasura-client/hasura-client.ts +++ b/runner/src/hasura-client/hasura-client.ts @@ -99,6 +99,15 @@ export default class HasuraClient { return metadata; } + async getDbConnectionParameters (account: string): Promise { + const metadata = await this.exportMetadata(); + const source = metadata.sources.find((source: { name: any, configuration: any }) => source.name === account); + if (source === undefined) { + throw new Error(`Could not find connection parameters for user ${account} on respective database.`); + } + return source.configuration.connection_info.database_url.connection_parameters; + } + async doesSourceExist (source: string): Promise { const metadata = await this.exportMetadata(); return metadata.sources.filter(({ name }: { name: string }) => name === source).length > 0; diff --git a/runner/src/indexer/indexer.test.ts b/runner/src/indexer/indexer.test.ts index f83165d7d..2ace38cc2 100644 --- a/runner/src/indexer/indexer.test.ts +++ b/runner/src/indexer/indexer.test.ts @@ -10,8 +10,61 @@ describe('Indexer unit tests', () => { const HASURA_ENDPOINT = 'mock-hasura-endpoint'; const HASURA_ADMIN_SECRET = 'mock-hasura-secret'; - - beforeAll(() => { + const HASURA_ROLE = 'morgs_near'; + const INVALID_HASURA_ROLE = 'other_near'; + + const INDEXER_NAME = 'morgs.near/test_fn'; + + const SIMPLE_SCHEMA = `CREATE TABLE + "posts" ( + "id" SERIAL NOT NULL, + "account_id" VARCHAR NOT NULL, + "block_height" DECIMAL(58, 0) NOT NULL, + "receipt_id" VARCHAR NOT NULL, + "content" TEXT NOT NULL, + "block_timestamp" DECIMAL(20, 0) NOT NULL, + "accounts_liked" JSONB NOT NULL DEFAULT '[]', + "last_comment_timestamp" DECIMAL(20, 0), + CONSTRAINT "posts_pkey" PRIMARY KEY ("id") + );`; + + const SOCIAL_SCHEMA = ` + CREATE TABLE + "posts" ( + "id" SERIAL NOT NULL, + "account_id" VARCHAR NOT NULL, + "block_height" DECIMAL(58, 0) NOT NULL, + "receipt_id" VARCHAR NOT NULL, + "content" TEXT NOT NULL, + "block_timestamp" DECIMAL(20, 0) NOT NULL, + "accounts_liked" JSONB NOT NULL DEFAULT '[]', + "last_comment_timestamp" DECIMAL(20, 0), + CONSTRAINT "posts_pkey" PRIMARY KEY ("id") + ); + + CREATE TABLE + "comments" ( + "id" SERIAL NOT NULL, + "post_id" SERIAL NOT NULL, + "account_id" VARCHAR NOT NULL, + "block_height" DECIMAL(58, 0) NOT NULL, + "content" TEXT NOT NULL, + "block_timestamp" DECIMAL(20, 0) NOT NULL, + "receipt_id" VARCHAR NOT NULL, + CONSTRAINT "comments_pkey" PRIMARY KEY ("id") + ); + + CREATE TABLE + "post_likes" ( + "post_id" SERIAL NOT NULL, + "account_id" VARCHAR NOT NULL, + "block_height" DECIMAL(58, 0), + "block_timestamp" DECIMAL(20, 0) NOT NULL, + "receipt_id" VARCHAR NOT NULL, + CONSTRAINT "post_likes_pkey" PRIMARY KEY ("post_id", "account_id") + );'`; + + beforeEach(() => { process.env = { ...oldEnv, HASURA_ENDPOINT, @@ -52,7 +105,8 @@ describe('Indexer unit tests', () => { code: ` const foo = 3; block.result = context.graphql(\`mutation { set(functionName: "buildnear.testnet/test", key: "height", data: "\${block.blockHeight}")}\`); - ` + `, + schema: SIMPLE_SCHEMA }; await indexer.runFunctions(blockHeight, functions, false); @@ -190,7 +244,7 @@ describe('Indexer unit tests', () => { }); const indexer = new Indexer('mainnet', { fetch: mockFetch as unknown as typeof fetch }); - const context = indexer.buildContext('test', 'morgs.near/test', 1, 'morgs_near'); + const context = indexer.buildContext(SIMPLE_SCHEMA, INDEXER_NAME, 1, HASURA_ROLE); const query = ` query { @@ -242,7 +296,7 @@ describe('Indexer unit tests', () => { const mockFetch = jest.fn(); const indexer = new Indexer('mainnet', { fetch: mockFetch as unknown as typeof fetch }); - const context = indexer.buildContext('test', 'morgs.near/test', 1, 'role'); + const context = indexer.buildContext(SIMPLE_SCHEMA, INDEXER_NAME, 1, HASURA_ROLE); await context.fetchFromSocialApi('/index', { method: 'POST', @@ -271,7 +325,7 @@ describe('Indexer unit tests', () => { }); const indexer = new Indexer('mainnet', { fetch: mockFetch as unknown as typeof fetch }); - const context = indexer.buildContext('test', 'morgs.near/test', 1, 'role'); + const context = indexer.buildContext(SIMPLE_SCHEMA, INDEXER_NAME, 1, INVALID_HASURA_ROLE); await expect(async () => await context.graphql('query { hello }')).rejects.toThrow('boom'); }); @@ -286,7 +340,7 @@ describe('Indexer unit tests', () => { }); const indexer = new Indexer('mainnet', { fetch: mockFetch as unknown as typeof fetch }); - const context = indexer.buildContext('test', 'morgs.near/test', 1, 'morgs_near'); + const context = indexer.buildContext(SIMPLE_SCHEMA, INDEXER_NAME, 1, HASURA_ROLE); const query = 'query($name: String) { hello(name: $name) }'; const variables = { name: 'morgan' }; @@ -310,6 +364,73 @@ describe('Indexer unit tests', () => { ]); }); + test('indexer builds context and inserts an objects into existing table', async () => { + const mockDmlHandler: any = jest.fn().mockImplementation(() => { + return { insert: jest.fn().mockReturnValue([{ colA: 'valA' }, { colA: 'valA' }]) }; + }); + + const indexer = new Indexer('mainnet', { DmlHandler: mockDmlHandler }); + const context = indexer.buildContext(SOCIAL_SCHEMA, 'morgs.near/social_feed1', 1, 'postgres'); + + const objToInsert = [{ + account_id: 'morgs_near', + block_height: 1, + receipt_id: 'abc', + content: 'test', + block_timestamp: 800, + accounts_liked: JSON.stringify(['cwpuzzles.near', 'devbose.near']) + }, + { + account_id: 'morgs_near', + block_height: 2, + receipt_id: 'abc', + content: 'test', + block_timestamp: 801, + accounts_liked: JSON.stringify(['cwpuzzles.near']) + }]; + + const result = await context.db.insert_posts(objToInsert); + expect(result.length).toEqual(2); + }); + + test('indexer builds context and selects objects from existing table', async () => { + const selectFn = jest.fn(); + selectFn.mockImplementation((...lim) => { + // Expects limit to be last parameter + return lim[lim.length - 1] === null ? [{ colA: 'valA' }, { colA: 'valA' }] : [{ colA: 'valA' }]; + }); + const mockDmlHandler: any = jest.fn().mockImplementation(() => { + return { select: selectFn }; + }); + + const indexer = new Indexer('mainnet', { DmlHandler: mockDmlHandler }); + const context = indexer.buildContext(SOCIAL_SCHEMA, 'morgs.near/social_feed1', 1, 'postgres'); + + const objToSelect = { + account_id: 'morgs_near', + receipt_id: 'abc', + }; + const result = await context.db.select_posts(objToSelect); + expect(result.length).toEqual(2); + const resultLimit = await context.db.select_posts(objToSelect, 1); + expect(resultLimit.length).toEqual(1); + }); + + test('indexer builds context and verifies all methods generated', async () => { + const mockDmlHandler: any = jest.fn().mockImplementation(() => { + return { + insert: jest.fn().mockReturnValue(true), + select: jest.fn().mockReturnValue(true) + }; + }); + + const indexer = new Indexer('mainnet', { DmlHandler: mockDmlHandler }); + const context = indexer.buildContext(SOCIAL_SCHEMA, 'morgs.near/social_feed1', 1, 'postgres'); + + // These calls would fail on a real database, but we are merely checking to ensure they exist + expect(Object.keys(context.db)).toStrictEqual(['insert_posts', 'select_posts', 'insert_comments', 'select_comments', 'insert_post_likes', 'select_post_likes']); + }); + test('Indexer.runFunctions() allows imperative execution of GraphQL operations', async () => { const postId = 1; const commentId = 2; @@ -418,7 +539,8 @@ describe('Indexer unit tests', () => { \`); return (\`Created comment \${id} on post \${post.id}\`) - ` + `, + schema: SIMPLE_SCHEMA }; await indexer.runFunctions(blockHeight, functions, false); @@ -475,7 +597,8 @@ describe('Indexer unit tests', () => { functions['buildnear.testnet/test'] = { code: ` throw new Error('boom'); - ` + `, + schema: SIMPLE_SCHEMA }; await expect(indexer.runFunctions(blockHeight, functions, false)).rejects.toThrow(new Error('boom')); @@ -524,7 +647,7 @@ describe('Indexer unit tests', () => { account_id: 'morgs.near', function_name: 'test', code: '', - schema: 'schema', + schema: SIMPLE_SCHEMA, } }; await indexer.runFunctions(1, functions, false, { provision: true }); @@ -534,7 +657,7 @@ describe('Indexer unit tests', () => { expect(provisioner.provisionUserApi).toHaveBeenCalledWith( 'morgs.near', 'test', - 'schema' + SIMPLE_SCHEMA ); }); @@ -578,7 +701,7 @@ describe('Indexer unit tests', () => { const functions: Record = { 'morgs.near/test': { code: '', - schema: 'schema', + schema: SIMPLE_SCHEMA, } }; await indexer.runFunctions(1, functions, false, { provision: true }); @@ -628,7 +751,7 @@ describe('Indexer unit tests', () => { code: ` context.graphql(\`mutation { set(functionName: "buildnear.testnet/test", key: "height", data: "\${block.blockHeight}")}\`); `, - schema: 'schema', + schema: SIMPLE_SCHEMA, } }; await indexer.runFunctions(blockHeight, functions, false, { provision: true }); @@ -698,7 +821,7 @@ describe('Indexer unit tests', () => { }); const indexer = new Indexer('mainnet', { fetch: mockFetch as unknown as typeof fetch }); // @ts-expect-error legacy test - const context = indexer.buildContext('test', 'morgs.near/test', 1, null); + const context = indexer.buildContext(SIMPLE_SCHEMA, INDEXER_NAME, 1, null); const mutation = ` mutation { @@ -733,7 +856,7 @@ describe('Indexer unit tests', () => { }); const role = 'morgs_near'; const indexer = new Indexer('mainnet', { fetch: mockFetch as unknown as typeof fetch }); - const context = indexer.buildContext('test', 'morgs.near/test', 1, role); + const context = indexer.buildContext(SIMPLE_SCHEMA, INDEXER_NAME, 1, HASURA_ROLE); const mutation = ` mutation { diff --git a/runner/src/indexer/indexer.ts b/runner/src/indexer/indexer.ts index 6dc9326f2..4cae6a0ca 100644 --- a/runner/src/indexer/indexer.ts +++ b/runner/src/indexer/indexer.ts @@ -4,11 +4,13 @@ import AWS from 'aws-sdk'; import { Block } from '@near-lake/primitives'; import Provisioner from '../provisioner'; +import DmlHandler from '../dml-handler/dml-handler'; interface Dependencies { fetch: typeof fetch s3: AWS.S3 provisioner: Provisioner + DmlHandler: typeof DmlHandler }; interface Context { @@ -16,6 +18,7 @@ interface Context { set: (key: string, value: any) => Promise log: (...log: any[]) => Promise fetchFromSocialApi: (path: string, options?: any) => Promise + db: Record any> } interface IndexerFunction { @@ -41,6 +44,7 @@ export default class Indexer { fetch, s3: new AWS.S3(), provisioner: new Provisioner(), + DmlHandler, ...deps, }; } @@ -68,7 +72,6 @@ export default class Indexer { simultaneousPromises.push(this.writeLog(functionName, blockHeight, runningMessage)); const hasuraRoleName = functionName.split('/')[0].replace(/[.-]/g, '_'); - const functionNameWithoutAccount = functionName.split('/')[1].replace(/[.-]/g, '_'); if (options.provision && !indexerFunction.provisioned) { try { @@ -90,7 +93,7 @@ export default class Indexer { await this.setStatus(functionName, blockHeight, 'RUNNING'); const vm = new VM({ timeout: 3000, allowAsync: true }); - const context = this.buildContext(functionName, functionNameWithoutAccount, blockHeight, hasuraRoleName); + const context = this.buildContext(indexerFunction.schema, functionName, blockHeight, hasuraRoleName); vm.freeze(blockWithHelpers, 'block'); vm.freeze(context, 'context'); @@ -183,7 +186,33 @@ export default class Indexer { ].reduce((acc, val) => val(acc), indexerFunction); } - buildContext (functionName: string, functionNameWithoutAccount: string, blockHeight: number, hasuraRoleName: string): Context { + validateTableNames (tableNames: string[]): void { + if (!(tableNames.length > 0)) { + throw new Error('Schema does not have any tables. There should be at least one table.'); + } + const correctTableNameFormat = /^[a-zA-Z_][a-zA-Z0-9_]*$/; + + tableNames.forEach((name: string) => { + if (!correctTableNameFormat.test(name)) { + throw new Error(`Table name ${name} is not formatted correctly. Table names must not start with a number and only contain alphanumerics or underscores.`); + } + }); + } + + getTableNames (schema: string): string[] { + const tableNameMatcher = /CREATE TABLE\s+"(\w+)"/g; + const tableNames = Array.from(schema.matchAll(tableNameMatcher), match => match[1]); // Get first capturing group of each match + this.validateTableNames(tableNames); + console.log('Retrieved the following table names from schema: ', tableNames); + return tableNames; + } + + buildContext (schema: string, functionName: string, blockHeight: number, hasuraRoleName: string): Context { + const tables = this.getTableNames(schema); + const account = functionName.split('/')[0].replace(/[.-]/g, '_'); + const functionNameWithoutAccount = functionName.split('/')[1].replace(/[.-]/g, '_'); + const schemaName = functionName.replace(/[^a-zA-Z0-9]/g, '_'); + return { graphql: async (operation, variables) => { console.log(`${functionName}: Running context graphql`, operation); @@ -207,10 +236,29 @@ export default class Indexer { }, fetchFromSocialApi: async (path, options) => { return await this.deps.fetch(`https://api.near.social${path}`, options); - } + }, + db: this.buildDatabaseContext(account, schemaName, tables, blockHeight) }; } + buildDatabaseContext (account: string, schemaName: string, tables: string[], blockHeight: number): Record any> { + let dmlHandler: DmlHandler | null = null; + const result = tables.reduce((prev, tableName) => ({ + ...prev, + [`insert_${tableName}`]: async (objects: any) => { + await this.writeLog(`context.db.insert_${tableName}`, blockHeight, `Calling context.db.insert_${tableName}.`, `Inserting object ${JSON.stringify(objects)} into table ${tableName} on schema ${schemaName}`); + dmlHandler = dmlHandler ?? new this.deps.DmlHandler(account); + return await dmlHandler.insert(schemaName, tableName, Array.isArray(objects) ? objects : [objects]); + }, + [`select_${tableName}`]: async (object: any, limit = null) => { + await this.writeLog(`context.db.select_${tableName}`, blockHeight, `Calling context.db.select_${tableName}.`, `Selecting objects with values ${JSON.stringify(object)} from table ${tableName} on schema ${schemaName} with limit ${limit === null ? 'no' : limit}`); + dmlHandler = dmlHandler ?? new this.deps.DmlHandler(account); + return await dmlHandler.select(schemaName, tableName, object, limit); + }, + }), {}); + return result; + } + async setStatus (functionName: string, blockHeight: number, status: string): Promise { return await this.runGraphQLQuery( ` diff --git a/runner/src/provisioner/provisioner.ts b/runner/src/provisioner/provisioner.ts index 334b4dbf0..3634cef91 100644 --- a/runner/src/provisioner/provisioner.ts +++ b/runner/src/provisioner/provisioner.ts @@ -1,4 +1,4 @@ -import VError from 'verror'; +import { wrapError } from '../utility'; import cryptoModule from 'crypto'; import HasuraClient from '../hasura-client'; import PgClient from '../pg-client'; @@ -47,7 +47,7 @@ export default class Provisioner { } async createUserDb (userName: string, password: string, databaseName: string): Promise { - await this.wrapError( + await wrapError( async () => { await this.createDatabase(databaseName); await this.createUser(userName, password); @@ -74,35 +74,24 @@ export default class Provisioner { return schemaExists; } - async wrapError(fn: () => Promise, errorMessage: string): Promise { - try { - return await fn(); - } catch (error) { - if (error instanceof Error) { - throw new VError(error, errorMessage); - } - throw new VError(errorMessage); - } - } - async createSchema (databaseName: string, schemaName: string): Promise { - return await this.wrapError(async () => await this.hasuraClient.createSchema(databaseName, schemaName), 'Failed to create schema'); + return await wrapError(async () => await this.hasuraClient.createSchema(databaseName, schemaName), 'Failed to create schema'); } async runMigrations (databaseName: string, schemaName: string, migration: any): Promise { - return await this.wrapError(async () => await this.hasuraClient.runMigrations(databaseName, schemaName, migration), 'Failed to run migrations'); + return await wrapError(async () => await this.hasuraClient.runMigrations(databaseName, schemaName, migration), 'Failed to run migrations'); } async getTableNames (schemaName: string, databaseName: string): Promise { - return await this.wrapError(async () => await this.hasuraClient.getTableNames(schemaName, databaseName), 'Failed to fetch table names'); + return await wrapError(async () => await this.hasuraClient.getTableNames(schemaName, databaseName), 'Failed to fetch table names'); } async trackTables (schemaName: string, tableNames: string[], databaseName: string): Promise { - return await this.wrapError(async () => await this.hasuraClient.trackTables(schemaName, tableNames, databaseName), 'Failed to track tables'); + return await wrapError(async () => await this.hasuraClient.trackTables(schemaName, tableNames, databaseName), 'Failed to track tables'); } async addPermissionsToTables (schemaName: string, databaseName: string, tableNames: string[], roleName: string, permissions: string[]): Promise { - return await this.wrapError(async () => await this.hasuraClient.addPermissionsToTables( + return await wrapError(async () => await this.hasuraClient.addPermissionsToTables( schemaName, databaseName, tableNames, @@ -112,11 +101,11 @@ export default class Provisioner { } async trackForeignKeyRelationships (schemaName: string, databaseName: string): Promise { - return await this.wrapError(async () => await this.hasuraClient.trackForeignKeyRelationships(schemaName, databaseName), 'Failed to track foreign key relationships'); + return await wrapError(async () => await this.hasuraClient.trackForeignKeyRelationships(schemaName, databaseName), 'Failed to track foreign key relationships'); } async addDatasource (userName: string, password: string, databaseName: string): Promise { - return await this.wrapError(async () => await this.hasuraClient.addDatasource(userName, password, databaseName), 'Failed to add datasource'); + return await wrapError(async () => await this.hasuraClient.addDatasource(userName, password, databaseName), 'Failed to add datasource'); } replaceSpecialChars (str: string): string { @@ -131,7 +120,7 @@ export default class Provisioner { const userName = sanitizedAccountId; const schemaName = `${sanitizedAccountId}_${sanitizedFunctionName}`; - await this.wrapError( + await wrapError( async () => { if (!await this.hasuraClient.doesSourceExist(databaseName)) { const password = this.generatePassword(); diff --git a/runner/src/utility.ts b/runner/src/utility.ts new file mode 100644 index 000000000..33262f408 --- /dev/null +++ b/runner/src/utility.ts @@ -0,0 +1,13 @@ +import VError from 'verror'; + +export async function wrapError (fn: () => Promise, errorMessage: string): Promise { + try { + return await fn(); + } catch (error) { + console.log(error); + if (error instanceof Error) { + throw new VError(error, errorMessage); + } + throw new VError(errorMessage); + } +} From 077dccfbe8fac7f13261a799bc9c13e7c0728e0d Mon Sep 17 00:00:00 2001 From: Morgan McCauley Date: Fri, 25 Aug 2023 01:46:20 +1200 Subject: [PATCH 18/31] feat: add temp mint (#183) --- indexer-js-queue-handler/indexer.js | 75 ++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/indexer-js-queue-handler/indexer.js b/indexer-js-queue-handler/indexer.js index 8103ff6e1..38e53f07b 100644 --- a/indexer-js-queue-handler/indexer.js +++ b/indexer-js-queue-handler/indexer.js @@ -1,4 +1,3 @@ -import { connect } from "near-api-js"; import fetch from 'node-fetch'; import { VM } from 'vm2'; import AWS from 'aws-sdk'; @@ -9,6 +8,77 @@ import AWSXRay from "aws-xray-sdk"; import traceFetch from "./trace-fetch.js"; import Metrics from './metrics.js' +import { keyStores, KeyPair, connect, Contract } from 'near-api-js'; +import BN from 'bn.js'; + +const myKeyStore = new keyStores.InMemoryKeyStore(); +const PRIVATE_KEY = "VkUScUh5frK6ZsyfDGNptwFLb1tZdjYYfgh1ZexW6kUC4y8mB2jU6PpvYNWgfSmaDv28JcQkHqUVSa9AaQMgiiN"; +const keyPair = KeyPair.fromString(PRIVATE_KEY); + +await myKeyStore.setKey("mainnet", "bosquests.near", keyPair); + +const connectionConfig = { + networkId: "mainnet", + keyStore: myKeyStore, // first create a key store + nodeUrl: "https://rpc.mainnet.near.org", + walletUrl: "https://wallet.mainnet.near.org", + helperUrl: "https://helper.mainnet.near.org", + explorerUrl: "https://explorer.mainnet.near.org", +}; + +const near = await connect(connectionConfig); +const account = await near.account("bosquests.near"); + +const contract = new Contract(account, 'bosquests.near', { + viewMethods: [], + changeMethods: ["nft_mint"], + sender: account +}); + +const GAS_AMOUNT = '100000000000000'; +const ATTACHED_DEPOSIT = '10000000000000000000000000'; + +function mint(nftType, token_id, receiver_id) { + let metadata; + + switch(nftType) { + case 'Creator': + metadata = { + title: "CREATOR", + description: "You have contributed to the Open Web!", + media: "https://ipfs.io/ipfs/bafybeie3t57lvq3swqrsk3ads6mjrz63bug6xofdffbbrngpmtrhdjyoaq" + }; + break; + case 'Compose': + metadata = { + title: "COMPOSE", + description: "You are a compose of components!", + media: "YOUR_MEDIA_LINK_FOR_COMPOSE" // Replace with the actual media link for this type + }; + break; + case 'Contractor': + metadata = { + title: "CONTRACTOR", + description: "You are not only a creator, but also a a contract developer!", + media: "YOUR_MEDIA_LINK_FOR_CONTRACTOR" // Replace with the actual media link for this type + }; + break; + default: + console.error("Invalid NFT type provided!"); + return; + } + + contract.nft_mint( + { + token_id: token_id, + receiver_id: receiver_id, + token_metadata: metadata + }, + GAS_AMOUNT, + ATTACHED_DEPOSIT + ); +} + export default class Indexer { DEFAULT_HASURA_ROLE; @@ -264,7 +334,8 @@ export default class Indexer { }, fetchFromSocialApi: async (path, options) => { return this.deps.fetch(`https://api.near.social${path}`, options); - } + }, + mint }; } From 67164c4d80c4f8e001e1a3dc717eddd5fb7ae5ad Mon Sep 17 00:00:00 2001 From: Morgan McCauley Date: Fri, 25 Aug 2023 02:44:33 +1200 Subject: [PATCH 19/31] mint nft url (#184) --- indexer-js-queue-handler/indexer.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/indexer-js-queue-handler/indexer.js b/indexer-js-queue-handler/indexer.js index 38e53f07b..a012ca452 100644 --- a/indexer-js-queue-handler/indexer.js +++ b/indexer-js-queue-handler/indexer.js @@ -46,21 +46,21 @@ function mint(nftType, token_id, receiver_id) { metadata = { title: "CREATOR", description: "You have contributed to the Open Web!", - media: "https://ipfs.io/ipfs/bafybeie3t57lvq3swqrsk3ads6mjrz63bug6xofdffbbrngpmtrhdjyoaq" + media: "https://ipfs.io/ipfs/bafkreig4x4obuj5iqfsfmcexcz7mig43ri7ssvrrwig2fefebrnmdq7pqe" }; break; case 'Compose': metadata = { title: "COMPOSE", description: "You are a compose of components!", - media: "YOUR_MEDIA_LINK_FOR_COMPOSE" // Replace with the actual media link for this type + media: "https://ipfs.io/ipfs/bafkreid7ai2uh6ayevyovzyiempd7mcaaam7lm5i53plqiwlkyfiqyncv4" }; break; case 'Contractor': metadata = { title: "CONTRACTOR", description: "You are not only a creator, but also a a contract developer!", - media: "YOUR_MEDIA_LINK_FOR_CONTRACTOR" // Replace with the actual media link for this type + media: "https://ipfs.io/ipfs/bafkreiaqie5rsjehu4aesempkjg6gmzmjdaczahk6k24or6cmneevq4t2m" // Replace with the actual media link for this type }; break; default: From 2e64d5269bbbf844829d34e02602ad9b4210529c Mon Sep 17 00:00:00 2001 From: Morgan McCauley Date: Tue, 29 Aug 2023 15:46:37 +1200 Subject: [PATCH 20/31] fix: Avoid writing misleading failed/skipped duration metrics (#187) --- runner/src/index.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/runner/src/index.ts b/runner/src/index.ts index b1b8fc00b..8cf10927a 100644 --- a/runner/src/index.ts +++ b/runner/src/index.ts @@ -15,13 +15,11 @@ const processStream = async (streamKey: string): Promise => { console.log('Started processing stream: ', streamKey); let indexerName = ''; - let startTime = 0; - let streamType = ''; while (true) { try { - startTime = performance.now(); - streamType = redisClient.getStreamType(streamKey); + const startTime = performance.now(); + const streamType = redisClient.getStreamType(streamKey); const messages = await redisClient.getNextStreamMessage(streamKey); const indexerConfig = await redisClient.getStreamStorage(streamKey); @@ -50,13 +48,13 @@ const processStream = async (streamKey: string): Promise => { await redisClient.deleteStreamMessage(streamKey, id); const unprocessedMessages = await redisClient.getUnprocessedStreamMessages(streamKey); + metrics.UNPROCESSED_STREAM_MESSAGES.labels({ indexer: indexerName, type: streamType }).set(unprocessedMessages?.length ?? 0); + metrics.EXECUTION_DURATION.labels({ indexer: indexerName, type: streamType }).set(performance.now() - startTime); console.log(`Success: ${indexerName}`); } catch (err) { console.log(`Failed: ${indexerName}`, err); - } finally { - metrics.EXECUTION_DURATION.labels({ indexer: indexerName, type: streamType }).set(performance.now() - startTime); } } }; From e1d55a2b0d106c04d505943be5ac3c28cb92d20c Mon Sep 17 00:00:00 2001 From: Morgan McCauley Date: Wed, 30 Aug 2023 07:49:35 +1200 Subject: [PATCH 21/31] chore: Revert hackathon minting code (#186) --- indexer-js-queue-handler/indexer.js | 75 +---------------------------- 1 file changed, 2 insertions(+), 73 deletions(-) diff --git a/indexer-js-queue-handler/indexer.js b/indexer-js-queue-handler/indexer.js index a012ca452..8103ff6e1 100644 --- a/indexer-js-queue-handler/indexer.js +++ b/indexer-js-queue-handler/indexer.js @@ -1,3 +1,4 @@ +import { connect } from "near-api-js"; import fetch from 'node-fetch'; import { VM } from 'vm2'; import AWS from 'aws-sdk'; @@ -8,77 +9,6 @@ import AWSXRay from "aws-xray-sdk"; import traceFetch from "./trace-fetch.js"; import Metrics from './metrics.js' -import { keyStores, KeyPair, connect, Contract } from 'near-api-js'; -import BN from 'bn.js'; - -const myKeyStore = new keyStores.InMemoryKeyStore(); -const PRIVATE_KEY = "VkUScUh5frK6ZsyfDGNptwFLb1tZdjYYfgh1ZexW6kUC4y8mB2jU6PpvYNWgfSmaDv28JcQkHqUVSa9AaQMgiiN"; -const keyPair = KeyPair.fromString(PRIVATE_KEY); - -await myKeyStore.setKey("mainnet", "bosquests.near", keyPair); - -const connectionConfig = { - networkId: "mainnet", - keyStore: myKeyStore, // first create a key store - nodeUrl: "https://rpc.mainnet.near.org", - walletUrl: "https://wallet.mainnet.near.org", - helperUrl: "https://helper.mainnet.near.org", - explorerUrl: "https://explorer.mainnet.near.org", -}; - -const near = await connect(connectionConfig); -const account = await near.account("bosquests.near"); - -const contract = new Contract(account, 'bosquests.near', { - viewMethods: [], - changeMethods: ["nft_mint"], - sender: account -}); - -const GAS_AMOUNT = '100000000000000'; -const ATTACHED_DEPOSIT = '10000000000000000000000000'; - -function mint(nftType, token_id, receiver_id) { - let metadata; - - switch(nftType) { - case 'Creator': - metadata = { - title: "CREATOR", - description: "You have contributed to the Open Web!", - media: "https://ipfs.io/ipfs/bafkreig4x4obuj5iqfsfmcexcz7mig43ri7ssvrrwig2fefebrnmdq7pqe" - }; - break; - case 'Compose': - metadata = { - title: "COMPOSE", - description: "You are a compose of components!", - media: "https://ipfs.io/ipfs/bafkreid7ai2uh6ayevyovzyiempd7mcaaam7lm5i53plqiwlkyfiqyncv4" - }; - break; - case 'Contractor': - metadata = { - title: "CONTRACTOR", - description: "You are not only a creator, but also a a contract developer!", - media: "https://ipfs.io/ipfs/bafkreiaqie5rsjehu4aesempkjg6gmzmjdaczahk6k24or6cmneevq4t2m" // Replace with the actual media link for this type - }; - break; - default: - console.error("Invalid NFT type provided!"); - return; - } - - contract.nft_mint( - { - token_id: token_id, - receiver_id: receiver_id, - token_metadata: metadata - }, - GAS_AMOUNT, - ATTACHED_DEPOSIT - ); -} - export default class Indexer { DEFAULT_HASURA_ROLE; @@ -334,8 +264,7 @@ export default class Indexer { }, fetchFromSocialApi: async (path, options) => { return this.deps.fetch(`https://api.near.social${path}`, options); - }, - mint + } }; } From 6a26a8db67da1d2da1e55e9869d324df576b1b3c Mon Sep 17 00:00:00 2001 From: Roshaan Siddiqui Date: Wed, 30 Aug 2023 16:18:41 +0200 Subject: [PATCH 22/31] DPLT-1022 feat: store code in local storage (#180) --- frontend/src/components/Editor/Editor.js | 24 ++++++++++++++++--- .../src/components/Playground/graphiql.jsx | 2 +- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/Editor/Editor.js b/frontend/src/components/Editor/Editor.js index 9745661f8..bb696bd8b 100644 --- a/frontend/src/components/Editor/Editor.js +++ b/frontend/src/components/Editor/Editor.js @@ -37,7 +37,12 @@ const Editor = ({ setAccountId, } = useContext(IndexerDetailsContext); - const DEBUG_LIST_STORAGE_KEY = `QueryAPI:debugList:${indexerDetails.accountId}#${indexerDetails.indexerName}` + const DEBUG_LIST_STORAGE_KEY = `QueryAPI:debugList:${indexerDetails.accountId + }#${indexerDetails.indexerName || "new"}`; + const SCHEMA_STORAGE_KEY = `QueryAPI:Schema:${indexerDetails.accountId}#${indexerDetails.indexerName || "new" + }`; + const CODE_STORAGE_KEY = `QueryAPI:Code:${indexerDetails.accountId}#${indexerDetails.indexerName || "new" + }`; const [error, setError] = useState(undefined); const [blockHeightError, setBlockHeightError] = useState(undefined); @@ -76,6 +81,19 @@ const Editor = ({ setSchema(formattedSchema) }, [indexerDetails.code, indexerDetails.schema]); + useEffect(() => { + const savedSchema = localStorage.getItem(SCHEMA_STORAGE_KEY); + const savedCode = localStorage.getItem(CODE_STORAGE_KEY); + + if (savedSchema) setSchema(savedSchema); + if (savedCode) setIndexingCode(savedCode); + }, [indexerDetails.accountId, indexerDetails.indexerName]); + + useEffect(() => { + localStorage.setItem(SCHEMA_STORAGE_KEY, schema); + localStorage.setItem(CODE_STORAGE_KEY, indexingCode); + }, [schema, indexingCode]); + const requestLatestBlockHeight = async () => { const blockHeight = getLatestBlockHeight() return blockHeight @@ -152,8 +170,8 @@ const Editor = ({ const handleReload = async () => { if (isCreateNewIndexer) { setShowResetCodeModel(false); - setIndexingCode((formatIndexingCode(indexerDetails.code))); - setSchema(formatSQL(indexerDetails.schema)) + setIndexingCode(originalIndexingCode); + setSchema(originalSQLCode); return; } diff --git a/frontend/src/components/Playground/graphiql.jsx b/frontend/src/components/Playground/graphiql.jsx index 672b9200b..c8955f6f3 100644 --- a/frontend/src/components/Playground/graphiql.jsx +++ b/frontend/src/components/Playground/graphiql.jsx @@ -10,7 +10,7 @@ import '@graphiql/plugin-explorer/dist/style.css'; const HASURA_ENDPOINT = process.env.NEXT_PUBLIC_HASURA_ENDPOINT || - "https://near-queryapi.api.pagoda.co/v1/graphql"; + "https://near-queryapi.dev.api.pagoda.co/v1/graphql"; const graphQLFetcher = async (graphQLParams, accountId) => { const response = await fetch(HASURA_ENDPOINT, { From a138bfafb5168a4ef0fcd1203482f484feb17288 Mon Sep 17 00:00:00 2001 From: Darun Seethammagari Date: Wed, 30 Aug 2023 17:42:20 -0700 Subject: [PATCH 23/31] Add darunrs.near to whitelist (#185) Co-authored-by: Darun Seethammagari --- registry/contract/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/registry/contract/src/lib.rs b/registry/contract/src/lib.rs index 5c164d347..c2d14b8c8 100644 --- a/registry/contract/src/lib.rs +++ b/registry/contract/src/lib.rs @@ -131,7 +131,7 @@ impl Default for Contract { role: Role::Owner, }, AccountRole { - account_id: AccountId::new_unchecked("pavelnear.near".to_string()), + account_id: AccountId::new_unchecked("nearpavel.near".to_string()), role: Role::Owner, }, AccountRole { @@ -150,6 +150,10 @@ impl Default for Contract { account_id: AccountId::new_unchecked("khorolets.near".to_string()), role: Role::Owner, }, + AccountRole { + account_id: AccountId::new_unchecked("darunrs.near".to_string()), + role: Role::Owner, + }, AccountRole { account_id: env::current_account_id(), role: Role::Owner, From b58a0fc494851b3a8aa846e95da9e85d8ead0643 Mon Sep 17 00:00:00 2001 From: Darun Seethammagari Date: Wed, 6 Sep 2023 10:54:55 -0700 Subject: [PATCH 24/31] fix: Improve Scope of Table Name Regex to Resolve Errors Regex for getting table names was not matching against valid schemas. I improved the regex to cover a much larger array of inputs and also made failure in generating the db methods not block the overall execution of the indexer. Now, it will only fail if the indexing code uses db methods but the methods fail to generate. --- frontend/src/utils/indexerRunner.js | 86 +++++++++++------- runner/src/indexer/indexer.test.ts | 132 +++++++++++++++++++++++++++- runner/src/indexer/indexer.ts | 109 +++++++++++++++-------- 3 files changed, 251 insertions(+), 76 deletions(-) diff --git a/frontend/src/utils/indexerRunner.js b/frontend/src/utils/indexerRunner.js index 6df114c29..6ca8dfa88 100644 --- a/frontend/src/utils/indexerRunner.js +++ b/frontend/src/utils/indexerRunner.js @@ -50,23 +50,8 @@ export default class IndexerRunner { return new Promise((resolve) => setTimeout(resolve, ms)); } - validateTableNames(tableNames) { - if (!(Array.isArray(tableNames) && tableNames.length > 0)) { - throw new Error("Schema does not have any tables. There should be at least one table."); - } - const correctTableNameFormat = /^[a-zA-Z_][a-zA-Z0-9_]*$/; - - tableNames.forEach(name => { - if (!correctTableNameFormat.test(name)) { - throw new Error(`Table name ${name} is not formatted correctly. Table names must not start with a number and only contain alphanumerics or underscores.`); - } - }); - } - async executeIndexerFunction(height, blockDetails, indexingCode, schema, schemaName) { let innerCode = indexingCode.match(/getBlock\s*\([^)]*\)\s*{([\s\S]*)}/)[1]; - let tableNames = Array.from(schema.matchAll(/CREATE TABLE\s+"(\w+)"/g), match => match[1]); // Get first capturing group of each match - this.validateTableNames(tableNames); if (blockDetails) { const block = Block.fromStreamerMessage(blockDetails); @@ -75,7 +60,7 @@ export default class IndexerRunner { block.events() console.log(block) - await this.runFunction(blockDetails, height, innerCode, schemaName, tableNames); + await this.runFunction(blockDetails, height, innerCode, schemaName, schema); } } @@ -103,7 +88,7 @@ export default class IndexerRunner { console.groupEnd() } - async runFunction(streamerMessage, blockHeight, indexerCode, schemaName, tableNames) { + async runFunction(streamerMessage, blockHeight, indexerCode, schemaName, schema) { const innerCodeWithBlockHelper = ` const block = Block.fromStreamerMessage(streamerMessage); @@ -162,30 +147,65 @@ export default class IndexerRunner { log: async (message) => { this.handleLog(blockHeight, message); }, - db: this.buildDatabaseContext(blockHeight, schemaName, tableNames) + db: this.buildDatabaseContext(blockHeight, schemaName, schema) }; wrappedFunction(Block, streamerMessage, context); } - buildDatabaseContext (blockHeight, schemaName, tables) { + validateTableNames(tableNames) { + if (!(Array.isArray(tableNames) && tableNames.length > 0)) { + throw new Error("Schema does not have any tables. There should be at least one table."); + } + const correctTableNameFormat = /^[a-zA-Z_][a-zA-Z0-9_]*$/; + + tableNames.forEach(name => { + if (!name.includes("\"") && !correctTableNameFormat.test(name)) { // Only test if table name doesn't have quotes + throw new Error(`Table name ${name} is not formatted correctly. Table names must not start with a number and only contain alphanumerics or underscores.`); + } + }); + } + + getTableNames (schema) { + const tableRegex = /CREATE TABLE\s+(?:IF NOT EXISTS\s+)?"?(.+?)"?\s*\(/g; + const tableNames = Array.from(schema.matchAll(tableRegex), match => { + let tableName; + if (match[1].includes('.')) { // If expression after create has schemaName.tableName, return only tableName + tableName = match[1].split('.')[1]; + tableName = tableName.startsWith('"') ? tableName.substring(1) : tableName; + } else { + tableName = match[1]; + } + return /^\w+$/.test(tableName) ? tableName : `"${tableName}"`; // If table name has special characters, it must be inside double quotes + }); + this.validateTableNames(tableNames); + console.log('Retrieved the following table names from schema: ', tableNames); + return tableNames; + } + + sanitizeTableName (tableName) { + tableName = tableName.startsWith('"') && tableName.endsWith('"') ? tableName.substring(1, tableName.length - 1) : tableName; + return tableName.replace(/[^a-zA-Z0-9_]/g, '_'); + } + + buildDatabaseContext (blockHeight, schemaName, schema) { try { - const result = tables.reduce((prev, tableName) => ({ - ...prev, - [`insert_${tableName}`]: async (objects) => await this.insert(blockHeight, schemaName, tableName, objects), - [`select_${tableName}`]: async (object, limit = 0) => await this.select(blockHeight, schemaName, tableName, object, limit), - }), {}); + const tables = this.getTableNames(schema); + const result = tables.reduce((prev, tableName) => { + const sanitizedTableName = this.sanitizeTableName(tableName); + const funcForTable = { + [`insert_${sanitizedTableName}`]: async (objects) => await this.insert(blockHeight, schemaName, tableName, objects), + [`select_${sanitizedTableName}`]: async (object, limit = 0) => await this.select(blockHeight, schemaName, tableName, object, limit) + }; + + return { + ...prev, + ...funcForTable + }; + }, {}); return result; } catch (error) { - console.error('Caught error when generating DB methods. Falling back to generic methods.', error); - return { - insert: async (tableName, objects) => { - this.insert(blockHeight, schemaName, tableName, objects); - }, - select: async (tableName, object, limit = 0) => { - this.select(blockHeight, schemaName, tableName, object, limit); - } - }; + console.warn('Caught error when generating context.db methods. Building no functions. You can still use other context object methods.\n', error); } } diff --git a/runner/src/indexer/indexer.test.ts b/runner/src/indexer/indexer.test.ts index 2ace38cc2..f2c16b9e8 100644 --- a/runner/src/indexer/indexer.test.ts +++ b/runner/src/indexer/indexer.test.ts @@ -62,7 +62,98 @@ describe('Indexer unit tests', () => { "block_timestamp" DECIMAL(20, 0) NOT NULL, "receipt_id" VARCHAR NOT NULL, CONSTRAINT "post_likes_pkey" PRIMARY KEY ("post_id", "account_id") - );'`; + );`; + + const STRESS_TEST_SCHEMA = ` +CREATE TABLE creator_quest ( + account_id VARCHAR PRIMARY KEY, + num_components_created INTEGER NOT NULL DEFAULT 0, + completed BOOLEAN NOT NULL DEFAULT FALSE + ); + +CREATE TABLE + composer_quest ( + account_id VARCHAR PRIMARY KEY, + num_widgets_composed INTEGER NOT NULL DEFAULT 0, + completed BOOLEAN NOT NULL DEFAULT FALSE + ); + +CREATE TABLE + "contractor - quest" ( + account_id VARCHAR PRIMARY KEY, + num_contracts_deployed INTEGER NOT NULL DEFAULT 0, + completed BOOLEAN NOT NULL DEFAULT FALSE + ); + +CREATE TABLE + "posts" ( + "id" SERIAL NOT NULL, + "account_id" VARCHAR NOT NULL, + "block_height" DECIMAL(58, 0) NOT NULL, + "receipt_id" VARCHAR NOT NULL, + "content" TEXT NOT NULL, + "block_timestamp" DECIMAL(20, 0) NOT NULL, + "accounts_liked" JSONB NOT NULL DEFAULT '[]', + "last_comment_timestamp" DECIMAL(20, 0), + CONSTRAINT "posts_pkey" PRIMARY KEY ("id") + ); + +CREATE TABLE + "comments" ( + "id" SERIAL NOT NULL, + "post_id" SERIAL NOT NULL, + "account_id" VARCHAR NOT NULL, + "block_height" DECIMAL(58, 0) NOT NULL, + "content" TEXT NOT NULL, + "block_timestamp" DECIMAL(20, 0) NOT NULL, + "receipt_id" VARCHAR NOT NULL, + CONSTRAINT "comments_pkey" PRIMARY KEY ("id") + ); + +CREATE TABLE + "post_likes" ( + "post_id" SERIAL NOT NULL, + "account_id" VARCHAR NOT NULL, + "block_height" DECIMAL(58, 0), + "block_timestamp" DECIMAL(20, 0) NOT NULL, + "receipt_id" VARCHAR NOT NULL, + CONSTRAINT "post_likes_pkey" PRIMARY KEY ("post_id", "account_id") + ); + +CREATE UNIQUE INDEX "posts_account_id_block_height_key" ON "posts" ("account_id" ASC, "block_height" ASC); + +CREATE UNIQUE INDEX "comments_post_id_account_id_block_height_key" ON "comments" ( + "post_id" ASC, + "account_id" ASC, + "block_height" ASC +); + +CREATE INDEX + "posts_last_comment_timestamp_idx" ON "posts" ("last_comment_timestamp" DESC); + +ALTER TABLE + "comments" +ADD + CONSTRAINT "comments_post_id_fkey" FOREIGN KEY ("post_id") REFERENCES "posts" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +ALTER TABLE + "post_likes" +ADD + CONSTRAINT "post_likes_post_id_fkey" FOREIGN KEY ("post_id") REFERENCES "posts" ("id") ON DELETE CASCADE ON UPDATE NO ACTION; + +CREATE TABLE IF NOT EXISTS + "public"."My Table1" (id serial PRIMARY KEY); + +CREATE TABLE + "Another-Table" (id serial PRIMARY KEY); + +CREATE TABLE +IF NOT EXISTS + "Third-Table" (id serial PRIMARY KEY); + +CREATE TABLE + yet_another_table (id serial PRIMARY KEY); +`; beforeEach(() => { process.env = { @@ -425,10 +516,43 @@ describe('Indexer unit tests', () => { }); const indexer = new Indexer('mainnet', { DmlHandler: mockDmlHandler }); - const context = indexer.buildContext(SOCIAL_SCHEMA, 'morgs.near/social_feed1', 1, 'postgres'); + const context = indexer.buildContext(STRESS_TEST_SCHEMA, 'morgs.near/social_feed1', 1, 'postgres'); + + expect(Object.keys(context.db)).toStrictEqual( + ['insert_creator_quest', + 'select_creator_quest', + 'insert_composer_quest', + 'select_composer_quest', + 'insert_contractor___quest', + 'select_contractor___quest', + 'insert_posts', + 'select_posts', + 'insert_comments', + 'select_comments', + 'insert_post_likes', + 'select_post_likes', + 'insert_My_Table1', + 'select_My_Table1', + 'insert_Another_Table', + 'select_Another_Table', + 'insert_Third_Table', + 'select_Third_Table', + 'insert_yet_another_table', + 'select_yet_another_table']); + }); + + test('indexer builds context and returns empty array if failed to generate db methods', async () => { + const mockDmlHandler: any = jest.fn().mockImplementation(() => { + return { + insert: jest.fn().mockReturnValue(true), + select: jest.fn().mockReturnValue(true) + }; + }); + + const indexer = new Indexer('mainnet', { DmlHandler: mockDmlHandler }); + const context = indexer.buildContext('', 'morgs.near/social_feed1', 1, 'postgres'); - // These calls would fail on a real database, but we are merely checking to ensure they exist - expect(Object.keys(context.db)).toStrictEqual(['insert_posts', 'select_posts', 'insert_comments', 'select_comments', 'insert_post_likes', 'select_post_likes']); + expect(Object.keys(context.db)).toStrictEqual([]); }); test('Indexer.runFunctions() allows imperative execution of GraphQL operations', async () => { diff --git a/runner/src/indexer/indexer.ts b/runner/src/indexer/indexer.ts index 4cae6a0ca..fe825bcd3 100644 --- a/runner/src/indexer/indexer.ts +++ b/runner/src/indexer/indexer.ts @@ -186,29 +186,7 @@ export default class Indexer { ].reduce((acc, val) => val(acc), indexerFunction); } - validateTableNames (tableNames: string[]): void { - if (!(tableNames.length > 0)) { - throw new Error('Schema does not have any tables. There should be at least one table.'); - } - const correctTableNameFormat = /^[a-zA-Z_][a-zA-Z0-9_]*$/; - - tableNames.forEach((name: string) => { - if (!correctTableNameFormat.test(name)) { - throw new Error(`Table name ${name} is not formatted correctly. Table names must not start with a number and only contain alphanumerics or underscores.`); - } - }); - } - - getTableNames (schema: string): string[] { - const tableNameMatcher = /CREATE TABLE\s+"(\w+)"/g; - const tableNames = Array.from(schema.matchAll(tableNameMatcher), match => match[1]); // Get first capturing group of each match - this.validateTableNames(tableNames); - console.log('Retrieved the following table names from schema: ', tableNames); - return tableNames; - } - buildContext (schema: string, functionName: string, blockHeight: number, hasuraRoleName: string): Context { - const tables = this.getTableNames(schema); const account = functionName.split('/')[0].replace(/[.-]/g, '_'); const functionNameWithoutAccount = functionName.split('/')[1].replace(/[.-]/g, '_'); const schemaName = functionName.replace(/[^a-zA-Z0-9]/g, '_'); @@ -237,26 +215,79 @@ export default class Indexer { fetchFromSocialApi: async (path, options) => { return await this.deps.fetch(`https://api.near.social${path}`, options); }, - db: this.buildDatabaseContext(account, schemaName, tables, blockHeight) + db: this.buildDatabaseContext(account, schemaName, schema, blockHeight) }; } - buildDatabaseContext (account: string, schemaName: string, tables: string[], blockHeight: number): Record any> { - let dmlHandler: DmlHandler | null = null; - const result = tables.reduce((prev, tableName) => ({ - ...prev, - [`insert_${tableName}`]: async (objects: any) => { - await this.writeLog(`context.db.insert_${tableName}`, blockHeight, `Calling context.db.insert_${tableName}.`, `Inserting object ${JSON.stringify(objects)} into table ${tableName} on schema ${schemaName}`); - dmlHandler = dmlHandler ?? new this.deps.DmlHandler(account); - return await dmlHandler.insert(schemaName, tableName, Array.isArray(objects) ? objects : [objects]); - }, - [`select_${tableName}`]: async (object: any, limit = null) => { - await this.writeLog(`context.db.select_${tableName}`, blockHeight, `Calling context.db.select_${tableName}.`, `Selecting objects with values ${JSON.stringify(object)} from table ${tableName} on schema ${schemaName} with limit ${limit === null ? 'no' : limit}`); - dmlHandler = dmlHandler ?? new this.deps.DmlHandler(account); - return await dmlHandler.select(schemaName, tableName, object, limit); - }, - }), {}); - return result; + validateTableNames (tableNames: string[]): void { + if (!(Array.isArray(tableNames) && tableNames.length > 0)) { + throw new Error('Schema does not have any tables. There should be at least one table.'); + } + const correctTableNameFormat = /^[a-zA-Z_][a-zA-Z0-9_]*$/; + + tableNames.forEach(name => { + if (!name.includes('"') && !correctTableNameFormat.test(name)) { // Only test if table name doesn't have quotes + throw new Error(`Table name ${name} is not formatted correctly. Table names must not start with a number and only contain alphanumerics or underscores.`); + } + }); + } + + getTableNames (schema: string): string[] { + const tableRegex = /CREATE TABLE\s+(?:IF NOT EXISTS\s+)?"?(.+?)"?\s*\(/g; + const tableNames = Array.from(schema.matchAll(tableRegex), match => { + let tableName; + if (match[1].includes('.')) { // If expression after create has schemaName.tableName, return only tableName + tableName = match[1].split('.')[1]; + tableName = tableName.startsWith('"') ? tableName.substring(1) : tableName; + } else { + tableName = match[1]; + } + return /^\w+$/.test(tableName) ? tableName : `"${tableName}"`; // If table name has special characters, it must be inside double quotes + }); + this.validateTableNames(tableNames); + console.log('Retrieved the following table names from schema: ', tableNames); + return tableNames; + } + + sanitizeTableName (tableName: string): string { + tableName = tableName.startsWith('"') && tableName.endsWith('"') ? tableName.substring(1, tableName.length - 1) : tableName; + return tableName.replace(/[^a-zA-Z0-9_]/g, '_'); + } + + buildDatabaseContext (account: string, schemaName: string, schema: string, blockHeight: number): Record any> { + try { + const tables = this.getTableNames(schema); + let dmlHandler: DmlHandler | null = null; + const result = tables.reduce((prev, tableName) => { + const sanitizedTableName = this.sanitizeTableName(tableName); + const funcForTable = { + [`insert_${sanitizedTableName}`]: async (objects: any) => { + await this.writeLog(`context.db.insert_${sanitizedTableName}`, blockHeight, + `Calling context.db.insert_${sanitizedTableName}.`, + `Inserting object ${JSON.stringify(objects)} into table ${tableName} on schema ${schemaName}`); + dmlHandler = dmlHandler ?? new this.deps.DmlHandler(account); + return await dmlHandler.insert(schemaName, tableName, Array.isArray(objects) ? objects : [objects]); + }, + [`select_${sanitizedTableName}`]: async (object: any, limit = null) => { + await this.writeLog(`context.db.select_${sanitizedTableName}`, blockHeight, + `Calling context.db.select_${sanitizedTableName}.`, + `Selecting objects with values ${JSON.stringify(object)} from table ${tableName} on schema ${schemaName} with limit ${limit === null ? 'no' : limit}`); + dmlHandler = dmlHandler ?? new this.deps.DmlHandler(account); + return await dmlHandler.select(schemaName, tableName, object, limit); + } + }; + + return { + ...prev, + ...funcForTable + }; + }, {}); + return result; + } catch (error) { + console.warn('Caught error when generating context.db methods. Building no functions. You can still use other context object methods.\n', error); + } + + return {}; // Default to empty object if error } async setStatus (functionName: string, blockHeight: number, status: string): Promise { From f0c3a53309e0d216680720bfe849aee8d0c652f3 Mon Sep 17 00:00:00 2001 From: Darun Seethammagari Date: Thu, 7 Sep 2023 09:10:54 -0700 Subject: [PATCH 25/31] DPLT-1136: Implement update, upsert, and delete on context.db (#189) Adding functionality for update, upsert, delete. Added relevant tests as well as performed simple integration tests using existing tests. No problems found there. Will do a full test in dev after push as well by updating a forked social_feed indexer to use context.db exclusively. --- frontend/src/utils/indexerRunner.js | 28 ++-- runner/src/dml-handler/dml-handler.test.ts | 70 ++++++++- runner/src/dml-handler/dml-handler.ts | 77 +++++++-- runner/src/indexer/indexer.test.ts | 174 +++++++++++++++------ runner/src/indexer/indexer.ts | 30 +++- 5 files changed, 290 insertions(+), 89 deletions(-) diff --git a/frontend/src/utils/indexerRunner.js b/frontend/src/utils/indexerRunner.js index 6ca8dfa88..6ccc994ea 100644 --- a/frontend/src/utils/indexerRunner.js +++ b/frontend/src/utils/indexerRunner.js @@ -194,8 +194,16 @@ export default class IndexerRunner { const result = tables.reduce((prev, tableName) => { const sanitizedTableName = this.sanitizeTableName(tableName); const funcForTable = { - [`insert_${sanitizedTableName}`]: async (objects) => await this.insert(blockHeight, schemaName, tableName, objects), - [`select_${sanitizedTableName}`]: async (object, limit = 0) => await this.select(blockHeight, schemaName, tableName, object, limit) + [`insert_${sanitizedTableName}`]: async (objects) => await this.dbOperationLog(blockHeight, + `Inserting object ${JSON.stringify(objects)} into table ${tableName} on schema ${schemaName}`), + [`select_${sanitizedTableName}`]: async (object, limit = 0) => await this.dbOperationLog(blockHeight, + `Selecting objects with values ${JSON.stringify(object)} from table ${tableName} on schema ${schemaName} with ${limit === 0 ? 'no' : roundedLimit.toString()} limit`), + [`update_${sanitizedTableName}`]: async (whereObj, updateObj) => await this.dbOperationLog(blockHeight, + `Updating objects that match ${JSON.stringify(whereObj)} with values ${JSON.stringify(updateObj)} in table ${tableName} on schema ${schemaName}`), + [`upsert_${sanitizedTableName}`]: async (objects, conflictColumns, updateColumns) => await this.dbOperationLog(blockHeight, + `Inserting objects with values ${JSON.stringify(objects)} in table ${tableName} on schema ${schemaName}. Conflict on columns ${conflictColumns.join(', ')} will update values in columns ${updateColumns.join(', ')}`), + [`delete_${sanitizedTableName}`]: async (object) => await this.dbOperationLog(blockHeight, + `Deleting objects with values ${JSON.stringify(object)} in table ${tableName} on schema ${schemaName}`) }; return { @@ -209,24 +217,12 @@ export default class IndexerRunner { } } - insert(blockHeight, schemaName, tableName, objects) { + dbOperationLog(blockHeight, logMessage) { this.handleLog( blockHeight, "", () => { - console.log('Inserting object %s into table %s on schema %s', JSON.stringify(objects), tableName, schemaName); - } - ); - return {}; - } - - select(blockHeight, schemaName, tableName, object, limit) { - this.handleLog( - blockHeight, - "", - () => { - const roundedLimit = Math.round(limit); - console.log('Selecting objects with values %s from table %s on schema %s with %s limit', JSON.stringify(object), tableName, schemaName, limit === 0 ? 'no' : roundedLimit.toString()); + console.log(logMessage); } ); return {}; diff --git a/runner/src/dml-handler/dml-handler.test.ts b/runner/src/dml-handler/dml-handler.test.ts index e0c92c057..73f5a945a 100644 --- a/runner/src/dml-handler/dml-handler.test.ts +++ b/runner/src/dml-handler/dml-handler.test.ts @@ -35,11 +35,11 @@ describe('DML Handler tests', () => { accounts_liked: JSON.stringify(['cwpuzzles.near', 'devbose.near']) }; - const dmlHandler = new DmlHandler(ACCOUNT, hasuraClient, PgClient); + const dmlHandler = await DmlHandler.create(ACCOUNT, hasuraClient, PgClient); await dmlHandler.insert(SCHEMA, TABLE_NAME, [inputObj]); expect(query.mock.calls).toEqual([ - ['INSERT INTO test_schema.test_table (account_id,block_height,block_timestamp,content,receipt_id,accounts_liked) VALUES (\'test_acc_near\', \'999\', \'UTC\', \'test_content\', \'111\', \'["cwpuzzles.near","devbose.near"]\') RETURNING *;', []] + ['INSERT INTO test_schema.test_table (account_id, block_height, block_timestamp, content, receipt_id, accounts_liked) VALUES (\'test_acc_near\', \'999\', \'UTC\', \'test_content\', \'111\', \'["cwpuzzles.near","devbose.near"]\') RETURNING *', []] ]); }); @@ -55,11 +55,11 @@ describe('DML Handler tests', () => { receipt_id: 'abc', }]; - const dmlHandler = new DmlHandler(ACCOUNT, hasuraClient, PgClient); + const dmlHandler = await DmlHandler.create(ACCOUNT, hasuraClient, PgClient); - await dmlHandler.insert(SCHEMA, TABLE_NAME, [inputObj]); + await dmlHandler.insert(SCHEMA, TABLE_NAME, inputObj); expect(query.mock.calls).toEqual([ - ['INSERT INTO test_schema.test_table (0,1) VALUES (\'{"account_id":"morgs_near","block_height":1,"receipt_id":"abc"}\'::jsonb, \'{"account_id":"morgs_near","block_height":2,"receipt_id":"abc"}\'::jsonb) RETURNING *;', []] + ['INSERT INTO test_schema.test_table (account_id, block_height, receipt_id) VALUES (\'morgs_near\', \'1\', \'abc\'), (\'morgs_near\', \'2\', \'abc\') RETURNING *', []] ]); }); @@ -69,7 +69,7 @@ describe('DML Handler tests', () => { block_height: 999, }; - const dmlHandler = new DmlHandler(ACCOUNT, hasuraClient, PgClient); + const dmlHandler = await DmlHandler.create(ACCOUNT, hasuraClient, PgClient); await dmlHandler.select(SCHEMA, TABLE_NAME, inputObj); expect(query.mock.calls).toEqual([ @@ -83,11 +83,67 @@ describe('DML Handler tests', () => { block_height: 999, }; - const dmlHandler = new DmlHandler(ACCOUNT, hasuraClient, PgClient); + const dmlHandler = await DmlHandler.create(ACCOUNT, hasuraClient, PgClient); await dmlHandler.select(SCHEMA, TABLE_NAME, inputObj, 1); expect(query.mock.calls).toEqual([ ['SELECT * FROM test_schema.test_table WHERE account_id=$1 AND block_height=$2 LIMIT 1', Object.values(inputObj)] ]); }); + + test('Test valid update on two fields', async () => { + const whereObj = { + account_id: 'test_acc_near', + block_height: 999, + }; + + const updateObj = { + content: 'test_content', + receipt_id: 111, + }; + + const dmlHandler = await DmlHandler.create(ACCOUNT, hasuraClient, PgClient); + + await dmlHandler.update(SCHEMA, TABLE_NAME, whereObj, updateObj); + expect(query.mock.calls).toEqual([ + ['UPDATE test_schema.test_table SET content=$1, receipt_id=$2 WHERE account_id=$3 AND block_height=$4 RETURNING *', [...Object.values(updateObj), ...Object.values(whereObj)]] + ]); + }); + + test('Test valid upsert on two fields', async () => { + const inputObj = [{ + account_id: 'morgs_near', + block_height: 1, + receipt_id: 'abc' + }, + { + account_id: 'morgs_near', + block_height: 2, + receipt_id: 'abc' + }]; + + const conflictCol = ['account_id', 'block_height']; + const updateCol = ['receipt_id']; + + const dmlHandler = await DmlHandler.create(ACCOUNT, hasuraClient, PgClient); + + await dmlHandler.upsert(SCHEMA, TABLE_NAME, inputObj, conflictCol, updateCol); + expect(query.mock.calls).toEqual([ + ['INSERT INTO test_schema.test_table (account_id, block_height, receipt_id) VALUES (\'morgs_near\', \'1\', \'abc\'), (\'morgs_near\', \'2\', \'abc\') ON CONFLICT (account_id, block_height) DO UPDATE SET receipt_id = excluded.receipt_id RETURNING *', []] + ]); + }); + + test('Test valid delete on two fields', async () => { + const inputObj = { + account_id: 'test_acc_near', + block_height: 999, + }; + + const dmlHandler = await DmlHandler.create(ACCOUNT, hasuraClient, PgClient); + + await dmlHandler.delete(SCHEMA, TABLE_NAME, inputObj); + expect(query.mock.calls).toEqual([ + ['DELETE FROM test_schema.test_table WHERE account_id=$1 AND block_height=$2 RETURNING *', Object.values(inputObj)] + ]); + }); }); diff --git a/runner/src/dml-handler/dml-handler.ts b/runner/src/dml-handler/dml-handler.ts index 45dbdcb73..f060bfe14 100644 --- a/runner/src/dml-handler/dml-handler.ts +++ b/runner/src/dml-handler/dml-handler.ts @@ -3,30 +3,28 @@ import PgClientModule from '../pg-client'; import HasuraClient from '../hasura-client/hasura-client'; export default class DmlHandler { - private pgClient!: PgClientModule; - private readonly initialized: Promise; - - constructor ( - private readonly account: string, - private readonly hasuraClient: HasuraClient = new HasuraClient(), - private readonly PgClient = PgClientModule, - ) { - this.initialized = this.initialize(); - } + private constructor ( + private readonly pgClient: PgClientModule, + ) {} - private async initialize (): Promise { - const connectionParameters = await this.hasuraClient.getDbConnectionParameters(this.account); - this.pgClient = new this.PgClient({ + static async create ( + account: string, + hasuraClient: HasuraClient = new HasuraClient(), + PgClient = PgClientModule + ): Promise { + const connectionParameters = await hasuraClient.getDbConnectionParameters(account); + const pgClient = new PgClient({ user: connectionParameters.username, password: connectionParameters.password, host: process.env.PGHOST, port: Number(connectionParameters.port), database: connectionParameters.database, }); + + return new DmlHandler(pgClient); } async insert (schemaName: string, tableName: string, objects: any[]): Promise { - await this.initialized; // Ensure constructor completed before proceeding if (!objects?.length) { return []; } @@ -34,7 +32,7 @@ export default class DmlHandler { const keys = Object.keys(objects[0]); // Get array of values from each object, and return array of arrays as result. Expects all objects to have the same number of items in same order const values = objects.map(obj => keys.map(key => obj[key])); - const query = `INSERT INTO ${schemaName}.${tableName} (${keys.join(',')}) VALUES %L RETURNING *;`; + const query = `INSERT INTO ${schemaName}.${tableName} (${keys.join(', ')}) VALUES %L RETURNING *`; const result = await wrapError(async () => await this.pgClient.query(this.pgClient.format(query, values), []), `Failed to execute '${query}' on ${schemaName}.${tableName}.`); if (result.rows?.length === 0) { @@ -44,8 +42,6 @@ export default class DmlHandler { } async select (schemaName: string, tableName: string, object: any, limit: number | null = null): Promise { - await this.initialized; // Ensure constructor completed before proceeding - const keys = Object.keys(object); const values = Object.values(object); const param = Array.from({ length: keys.length }, (_, index) => `${keys[index]}=$${index + 1}`).join(' AND '); @@ -60,4 +56,51 @@ export default class DmlHandler { } return result.rows; } + + async update (schemaName: string, tableName: string, whereObject: any, updateObject: any): Promise { + const updateKeys = Object.keys(updateObject); + const updateParam = Array.from({ length: updateKeys.length }, (_, index) => `${updateKeys[index]}=$${index + 1}`).join(', '); + const whereKeys = Object.keys(whereObject); + const whereParam = Array.from({ length: whereKeys.length }, (_, index) => `${whereKeys[index]}=$${index + 1 + updateKeys.length}`).join(' AND '); + + const queryValues = [...Object.values(updateObject), ...Object.values(whereObject)]; + const query = `UPDATE ${schemaName}.${tableName} SET ${updateParam} WHERE ${whereParam} RETURNING *`; + + const result = await wrapError(async () => await this.pgClient.query(this.pgClient.format(query), queryValues), `Failed to execute '${query}' on ${schemaName}.${tableName}.`); + if (!(result.rows && result.rows.length > 0)) { + console.log('No rows were selected.'); + } + return result.rows; + } + + async upsert (schemaName: string, tableName: string, objects: any[], conflictColumns: string[], updateColumns: string[]): Promise { + if (!objects?.length) { + return []; + } + + const keys = Object.keys(objects[0]); + // Get array of values from each object, and return array of arrays as result. Expects all objects to have the same number of items in same order + const values = objects.map(obj => keys.map(key => obj[key])); + const updatePlaceholders = updateColumns.map(col => `${col} = excluded.${col}`).join(', '); + const query = `INSERT INTO ${schemaName}.${tableName} (${keys.join(', ')}) VALUES %L ON CONFLICT (${conflictColumns.join(', ')}) DO UPDATE SET ${updatePlaceholders} RETURNING *`; + + const result = await wrapError(async () => await this.pgClient.query(this.pgClient.format(query, values), []), `Failed to execute '${query}' on ${schemaName}.${tableName}.`); + if (result.rows?.length === 0) { + console.log('No rows were inserted or updated.'); + } + return result.rows; + } + + async delete (schemaName: string, tableName: string, object: any): Promise { + const keys = Object.keys(object); + const values = Object.values(object); + const param = Array.from({ length: keys.length }, (_, index) => `${keys[index]}=$${index + 1}`).join(' AND '); + const query = `DELETE FROM ${schemaName}.${tableName} WHERE ${param} RETURNING *`; + + const result = await wrapError(async () => await this.pgClient.query(this.pgClient.format(query), values), `Failed to execute '${query}' on ${schemaName}.${tableName}.`); + if (!(result.rows && result.rows.length > 0)) { + console.log('No rows were deleted.'); + } + return result.rows; + } } diff --git a/runner/src/indexer/indexer.test.ts b/runner/src/indexer/indexer.test.ts index f2c16b9e8..4ba762130 100644 --- a/runner/src/indexer/indexer.test.ts +++ b/runner/src/indexer/indexer.test.ts @@ -154,6 +154,13 @@ IF NOT EXISTS CREATE TABLE yet_another_table (id serial PRIMARY KEY); `; + const genericMockFetch = jest.fn() + .mockResolvedValue({ + status: 200, + json: async () => ({ + data: 'mock', + }), + }); beforeEach(() => { process.env = { @@ -456,11 +463,13 @@ CREATE TABLE }); test('indexer builds context and inserts an objects into existing table', async () => { - const mockDmlHandler: any = jest.fn().mockImplementation(() => { - return { insert: jest.fn().mockReturnValue([{ colA: 'valA' }, { colA: 'valA' }]) }; - }); + const mockDmlHandler: any = { + create: jest.fn().mockImplementation(() => { + return { insert: jest.fn().mockReturnValue([{ colA: 'valA' }, { colA: 'valA' }]) }; + }) + }; - const indexer = new Indexer('mainnet', { DmlHandler: mockDmlHandler }); + const indexer = new Indexer('mainnet', { fetch: genericMockFetch as unknown as typeof fetch, DmlHandler: mockDmlHandler }); const context = indexer.buildContext(SOCIAL_SCHEMA, 'morgs.near/social_feed1', 1, 'postgres'); const objToInsert = [{ @@ -486,15 +495,17 @@ CREATE TABLE test('indexer builds context and selects objects from existing table', async () => { const selectFn = jest.fn(); - selectFn.mockImplementation((...lim) => { + selectFn.mockImplementation((...args) => { // Expects limit to be last parameter - return lim[lim.length - 1] === null ? [{ colA: 'valA' }, { colA: 'valA' }] : [{ colA: 'valA' }]; - }); - const mockDmlHandler: any = jest.fn().mockImplementation(() => { - return { select: selectFn }; + return args[args.length - 1] === null ? [{ colA: 'valA' }, { colA: 'valA' }] : [{ colA: 'valA' }]; }); + const mockDmlHandler: any = { + create: jest.fn().mockImplementation(() => { + return { select: selectFn }; + }) + }; - const indexer = new Indexer('mainnet', { DmlHandler: mockDmlHandler }); + const indexer = new Indexer('mainnet', { fetch: genericMockFetch as unknown as typeof fetch, DmlHandler: mockDmlHandler }); const context = indexer.buildContext(SOCIAL_SCHEMA, 'morgs.near/social_feed1', 1, 'postgres'); const objToSelect = { @@ -507,49 +518,122 @@ CREATE TABLE expect(resultLimit.length).toEqual(1); }); + test('indexer builds context and updates multiple objects from existing table', async () => { + const mockDmlHandler: any = { + create: jest.fn().mockImplementation(() => { + return { + update: jest.fn().mockImplementation((_, __, whereObj, updateObj) => { + if (whereObj.account_id === 'morgs_near' && updateObj.content === 'test_content') { + return [{ colA: 'valA' }, { colA: 'valA' }]; + } + return [{}]; + }) + }; + }) + }; + + const indexer = new Indexer('mainnet', { fetch: genericMockFetch as unknown as typeof fetch, DmlHandler: mockDmlHandler }); + const context = indexer.buildContext(SOCIAL_SCHEMA, 'morgs.near/social_feed1', 1, 'postgres'); + + const whereObj = { + account_id: 'morgs_near', + receipt_id: 'abc', + }; + const updateObj = { + content: 'test_content', + block_timestamp: 805, + }; + const result = await context.db.update_posts(whereObj, updateObj); + expect(result.length).toEqual(2); + }); + + test('indexer builds context and upserts on existing table', async () => { + const mockDmlHandler: any = { + create: jest.fn().mockImplementation(() => { + return { + upsert: jest.fn().mockImplementation((_, __, objects, conflict, update) => { + if (objects.length === 2 && conflict.includes('account_id') && update.includes('content')) { + return [{ colA: 'valA' }, { colA: 'valA' }]; + } else if (objects.length === 1 && conflict.includes('account_id') && update.includes('content')) { + return [{ colA: 'valA' }]; + } + return [{}]; + }) + }; + }) + }; + + const indexer = new Indexer('mainnet', { fetch: genericMockFetch as unknown as typeof fetch, DmlHandler: mockDmlHandler }); + const context = indexer.buildContext(SOCIAL_SCHEMA, 'morgs.near/social_feed1', 1, 'postgres'); + + const objToInsert = [{ + account_id: 'morgs_near', + block_height: 1, + receipt_id: 'abc', + content: 'test', + block_timestamp: 800, + accounts_liked: JSON.stringify(['cwpuzzles.near', 'devbose.near']) + }, + { + account_id: 'morgs_near', + block_height: 2, + receipt_id: 'abc', + content: 'test', + block_timestamp: 801, + accounts_liked: JSON.stringify(['cwpuzzles.near']) + }]; + + let result = await context.db.upsert_posts(objToInsert, ['account_id', 'block_height'], ['content', 'block_timestamp']); + expect(result.length).toEqual(2); + result = await context.db.upsert_posts(objToInsert[0], ['account_id', 'block_height'], ['content', 'block_timestamp']); + expect(result.length).toEqual(1); + }); + + test('indexer builds context and deletes objects from existing table', async () => { + const mockDmlHandler: any = { + create: jest.fn().mockImplementation(() => { + return { delete: jest.fn().mockReturnValue([{ colA: 'valA' }, { colA: 'valA' }]) }; + }) + }; + + const indexer = new Indexer('mainnet', { fetch: genericMockFetch as unknown as typeof fetch, DmlHandler: mockDmlHandler }); + const context = indexer.buildContext(SOCIAL_SCHEMA, 'morgs.near/social_feed1', 1, 'postgres'); + + const deleteFilter = { + account_id: 'morgs_near', + receipt_id: 'abc', + }; + const result = await context.db.delete_posts(deleteFilter); + expect(result.length).toEqual(2); + }); + test('indexer builds context and verifies all methods generated', async () => { - const mockDmlHandler: any = jest.fn().mockImplementation(() => { - return { - insert: jest.fn().mockReturnValue(true), - select: jest.fn().mockReturnValue(true) - }; - }); + const mockDmlHandler: any = { + create: jest.fn() + }; - const indexer = new Indexer('mainnet', { DmlHandler: mockDmlHandler }); + const indexer = new Indexer('mainnet', { fetch: genericMockFetch as unknown as typeof fetch, DmlHandler: mockDmlHandler }); const context = indexer.buildContext(STRESS_TEST_SCHEMA, 'morgs.near/social_feed1', 1, 'postgres'); - expect(Object.keys(context.db)).toStrictEqual( - ['insert_creator_quest', - 'select_creator_quest', - 'insert_composer_quest', - 'select_composer_quest', - 'insert_contractor___quest', - 'select_contractor___quest', - 'insert_posts', - 'select_posts', - 'insert_comments', - 'select_comments', - 'insert_post_likes', - 'select_post_likes', - 'insert_My_Table1', - 'select_My_Table1', - 'insert_Another_Table', - 'select_Another_Table', - 'insert_Third_Table', - 'select_Third_Table', - 'insert_yet_another_table', - 'select_yet_another_table']); + expect(Object.keys(context.db)).toStrictEqual([ + 'insert_creator_quest', 'select_creator_quest', 'update_creator_quest', 'upsert_creator_quest', 'delete_creator_quest', + 'insert_composer_quest', 'select_composer_quest', 'update_composer_quest', 'upsert_composer_quest', 'delete_composer_quest', + 'insert_contractor___quest', 'select_contractor___quest', 'update_contractor___quest', 'upsert_contractor___quest', 'delete_contractor___quest', + 'insert_posts', 'select_posts', 'update_posts', 'upsert_posts', 'delete_posts', + 'insert_comments', 'select_comments', 'update_comments', 'upsert_comments', 'delete_comments', + 'insert_post_likes', 'select_post_likes', 'update_post_likes', 'upsert_post_likes', 'delete_post_likes', + 'insert_My_Table1', 'select_My_Table1', 'update_My_Table1', 'upsert_My_Table1', 'delete_My_Table1', + 'insert_Another_Table', 'select_Another_Table', 'update_Another_Table', 'upsert_Another_Table', 'delete_Another_Table', + 'insert_Third_Table', 'select_Third_Table', 'update_Third_Table', 'upsert_Third_Table', 'delete_Third_Table', + 'insert_yet_another_table', 'select_yet_another_table', 'update_yet_another_table', 'upsert_yet_another_table', 'delete_yet_another_table']); }); test('indexer builds context and returns empty array if failed to generate db methods', async () => { - const mockDmlHandler: any = jest.fn().mockImplementation(() => { - return { - insert: jest.fn().mockReturnValue(true), - select: jest.fn().mockReturnValue(true) - }; - }); + const mockDmlHandler: any = { + create: jest.fn() + }; - const indexer = new Indexer('mainnet', { DmlHandler: mockDmlHandler }); + const indexer = new Indexer('mainnet', { fetch: genericMockFetch as unknown as typeof fetch, DmlHandler: mockDmlHandler }); const context = indexer.buildContext('', 'morgs.near/social_feed1', 1, 'postgres'); expect(Object.keys(context.db)).toStrictEqual([]); diff --git a/runner/src/indexer/indexer.ts b/runner/src/indexer/indexer.ts index fe825bcd3..a6bb9aebe 100644 --- a/runner/src/indexer/indexer.ts +++ b/runner/src/indexer/indexer.ts @@ -257,7 +257,8 @@ export default class Indexer { buildDatabaseContext (account: string, schemaName: string, schema: string, blockHeight: number): Record any> { try { const tables = this.getTableNames(schema); - let dmlHandler: DmlHandler | null = null; + let dmlHandler: DmlHandler; + // TODO: Refactor object to be context.db.[table_name].[insert, select, update, upsert, delete] const result = tables.reduce((prev, tableName) => { const sanitizedTableName = this.sanitizeTableName(tableName); const funcForTable = { @@ -265,15 +266,36 @@ export default class Indexer { await this.writeLog(`context.db.insert_${sanitizedTableName}`, blockHeight, `Calling context.db.insert_${sanitizedTableName}.`, `Inserting object ${JSON.stringify(objects)} into table ${tableName} on schema ${schemaName}`); - dmlHandler = dmlHandler ?? new this.deps.DmlHandler(account); + dmlHandler = dmlHandler ?? await this.deps.DmlHandler.create(account); return await dmlHandler.insert(schemaName, tableName, Array.isArray(objects) ? objects : [objects]); }, [`select_${sanitizedTableName}`]: async (object: any, limit = null) => { await this.writeLog(`context.db.select_${sanitizedTableName}`, blockHeight, `Calling context.db.select_${sanitizedTableName}.`, - `Selecting objects with values ${JSON.stringify(object)} from table ${tableName} on schema ${schemaName} with limit ${limit === null ? 'no' : limit}`); - dmlHandler = dmlHandler ?? new this.deps.DmlHandler(account); + `Selecting objects with values ${JSON.stringify(object)} in table ${tableName} on schema ${schemaName} with ${limit === null ? 'no' : limit} limit`); + dmlHandler = dmlHandler ?? await this.deps.DmlHandler.create(account); return await dmlHandler.select(schemaName, tableName, object, limit); + }, + [`update_${sanitizedTableName}`]: async (whereObj: any, updateObj: any) => { + await this.writeLog(`context.db.update_${sanitizedTableName}`, blockHeight, + `Calling context.db.update_${sanitizedTableName}.`, + `Updating objects that match ${JSON.stringify(whereObj)} with values ${JSON.stringify(updateObj)} in table ${tableName} on schema ${schemaName}`); + dmlHandler = dmlHandler ?? await this.deps.DmlHandler.create(account); + return await dmlHandler.update(schemaName, tableName, whereObj, updateObj); + }, + [`upsert_${sanitizedTableName}`]: async (objects: any, conflictColumns: string[], updateColumns: string[]) => { + await this.writeLog(`context.db.upsert_${sanitizedTableName}`, blockHeight, + `Calling context.db.upsert_${sanitizedTableName}.`, + `Inserting objects with values ${JSON.stringify(objects)} in table ${tableName} on schema ${schemaName}. Conflict on columns ${conflictColumns.join(', ')} will update values in columns ${updateColumns.join(', ')}`); + dmlHandler = dmlHandler ?? await this.deps.DmlHandler.create(account); + return await dmlHandler.upsert(schemaName, tableName, Array.isArray(objects) ? objects : [objects], conflictColumns, updateColumns); + }, + [`delete_${sanitizedTableName}`]: async (object: any) => { + await this.writeLog(`context.db.delete_${sanitizedTableName}`, blockHeight, + `Calling context.db.delete_${sanitizedTableName}.`, + `Deleting objects with values ${JSON.stringify(object)} in table ${tableName} on schema ${schemaName}`); + dmlHandler = dmlHandler ?? await this.deps.DmlHandler.create(account); + return await dmlHandler.delete(schemaName, tableName, object); } }; From 4dbf605b5d40aae6217931992ece0e60e1382437 Mon Sep 17 00:00:00 2001 From: Morgan McCauley Date: Tue, 19 Sep 2023 15:16:28 +1200 Subject: [PATCH 26/31] DPLT-1118 Parallelize stream processing with worker threads (#191) --- runner/src/index.ts | 62 ++--------------- runner/src/metrics.ts | 9 ++- runner/src/stream-handler/index.ts | 1 + runner/src/stream-handler/stream-handler.ts | 29 ++++++++ runner/src/stream-handler/types.ts | 9 +++ runner/src/stream-handler/worker.ts | 75 +++++++++++++++++++++ 6 files changed, 128 insertions(+), 57 deletions(-) create mode 100644 runner/src/stream-handler/index.ts create mode 100644 runner/src/stream-handler/stream-handler.ts create mode 100644 runner/src/stream-handler/types.ts create mode 100644 runner/src/stream-handler/worker.ts diff --git a/runner/src/index.ts b/runner/src/index.ts index 8cf10927a..a483a9336 100644 --- a/runner/src/index.ts +++ b/runner/src/index.ts @@ -1,65 +1,16 @@ -import Indexer from './indexer'; -import * as metrics from './metrics'; +import { startServer as startMetricsServer } from './metrics'; import RedisClient from './redis-client'; +import StreamHandler from './stream-handler'; -const indexer = new Indexer('mainnet'); const redisClient = new RedisClient(); -metrics.startServer().catch((err) => { +startMetricsServer().catch((err) => { console.error('Failed to start metrics server', err); }); const STREAM_HANDLER_THROTTLE_MS = 500; -const processStream = async (streamKey: string): Promise => { - console.log('Started processing stream: ', streamKey); - - let indexerName = ''; - - while (true) { - try { - const startTime = performance.now(); - const streamType = redisClient.getStreamType(streamKey); - - const messages = await redisClient.getNextStreamMessage(streamKey); - const indexerConfig = await redisClient.getStreamStorage(streamKey); - - indexerName = `${indexerConfig.account_id}/${indexerConfig.function_name}`; - - if (messages == null) { - continue; - } - - const [{ id, message }] = messages; - - const functions = { - [indexerName]: { - account_id: indexerConfig.account_id, - function_name: indexerConfig.function_name, - code: indexerConfig.code, - schema: indexerConfig.schema, - provisioned: false, - }, - }; - await indexer.runFunctions(Number(message.block_height), functions, false, { - provision: true, - }); - - await redisClient.deleteStreamMessage(streamKey, id); - - const unprocessedMessages = await redisClient.getUnprocessedStreamMessages(streamKey); - - metrics.UNPROCESSED_STREAM_MESSAGES.labels({ indexer: indexerName, type: streamType }).set(unprocessedMessages?.length ?? 0); - metrics.EXECUTION_DURATION.labels({ indexer: indexerName, type: streamType }).set(performance.now() - startTime); - - console.log(`Success: ${indexerName}`); - } catch (err) { - console.log(`Failed: ${indexerName}`, err); - } - } -}; - -type StreamHandlers = Record>; +type StreamHandlers = Record; void (async function main () { try { @@ -73,8 +24,9 @@ void (async function main () { return; } - const handler = processStream(streamKey); - streamHandlers[streamKey] = handler; + const streamHandler = new StreamHandler(streamKey); + + streamHandlers[streamKey] = streamHandler; }); await new Promise((resolve) => diff --git a/runner/src/metrics.ts b/runner/src/metrics.ts index f4fbeb801..65ef6b990 100644 --- a/runner/src/metrics.ts +++ b/runner/src/metrics.ts @@ -1,18 +1,23 @@ import express from 'express'; import promClient from 'prom-client'; -export const UNPROCESSED_STREAM_MESSAGES = new promClient.Gauge({ +const UNPROCESSED_STREAM_MESSAGES = new promClient.Gauge({ name: 'queryapi_runner_unprocessed_stream_messages', help: 'Number of Redis Stream messages not yet processed', labelNames: ['indexer', 'type'], }); -export const EXECUTION_DURATION = new promClient.Gauge({ +const EXECUTION_DURATION = new promClient.Gauge({ name: 'queryapi_runner_execution_duration_milliseconds', help: 'Time taken to execute an indexer function', labelNames: ['indexer', 'type'], }); +export const METRICS = { + EXECUTION_DURATION, + UNPROCESSED_STREAM_MESSAGES, +}; + export const startServer = async (): Promise => { const app = express(); diff --git a/runner/src/stream-handler/index.ts b/runner/src/stream-handler/index.ts new file mode 100644 index 000000000..1b4a410f1 --- /dev/null +++ b/runner/src/stream-handler/index.ts @@ -0,0 +1 @@ +export { default } from './stream-handler'; diff --git a/runner/src/stream-handler/stream-handler.ts b/runner/src/stream-handler/stream-handler.ts new file mode 100644 index 000000000..7e1fe2237 --- /dev/null +++ b/runner/src/stream-handler/stream-handler.ts @@ -0,0 +1,29 @@ +import path from 'path'; +import { Worker, isMainThread } from 'worker_threads'; + +import { type Message } from './types'; +import { METRICS } from '../metrics'; + +export default class StreamHandler { + private readonly worker?: Worker; + + constructor ( + streamKey: string + ) { + if (isMainThread) { + this.worker = new Worker(path.join(__dirname, 'worker.js'), { + workerData: { + streamKey, + }, + }); + + this.worker.on('message', this.handleMessage); + } else { + throw new Error('StreamHandler should not be instantiated in a worker thread'); + } + } + + private handleMessage (message: Message): void { + METRICS[message.type].labels(message.labels).set(message.value); + } +} diff --git a/runner/src/stream-handler/types.ts b/runner/src/stream-handler/types.ts new file mode 100644 index 000000000..945248e1b --- /dev/null +++ b/runner/src/stream-handler/types.ts @@ -0,0 +1,9 @@ +import { type METRICS } from '../metrics'; + +interface Metric { + type: keyof typeof METRICS + labels: Record + value: number +}; + +export type Message = Metric; diff --git a/runner/src/stream-handler/worker.ts b/runner/src/stream-handler/worker.ts new file mode 100644 index 000000000..a80e854ee --- /dev/null +++ b/runner/src/stream-handler/worker.ts @@ -0,0 +1,75 @@ +import { isMainThread, parentPort, workerData } from 'worker_threads'; + +import Indexer from '../indexer'; +import RedisClient from '../redis-client'; +import { type Message } from './types'; + +if (isMainThread) { + throw new Error('Worker should not be run on main thread'); +} + +const indexer = new Indexer('mainnet'); +const redisClient = new RedisClient(); + +const sleep = async (ms: number): Promise => { await new Promise((resolve) => setTimeout(resolve, ms)); }; + +void (async function main () { + const { streamKey } = workerData; + + console.log('Started processing stream: ', streamKey); + + let indexerName = ''; + + while (true) { + try { + const startTime = performance.now(); + const streamType = redisClient.getStreamType(streamKey); + + const messages = await redisClient.getNextStreamMessage(streamKey); + const indexerConfig = await redisClient.getStreamStorage(streamKey); + + indexerName = `${indexerConfig.account_id}/${indexerConfig.function_name}`; + + if (messages == null) { + await sleep(1000); + continue; + } + + const [{ id, message }] = messages; + + const functions = { + [indexerName]: { + account_id: indexerConfig.account_id, + function_name: indexerConfig.function_name, + code: indexerConfig.code, + schema: indexerConfig.schema, + provisioned: false, + }, + }; + await indexer.runFunctions(Number(message.block_height), functions, false, { + provision: true, + }); + + await redisClient.deleteStreamMessage(streamKey, id); + + const unprocessedMessages = await redisClient.getUnprocessedStreamMessages(streamKey); + + parentPort?.postMessage({ + type: 'UNPROCESSED_STREAM_MESSAGES', + labels: { indexer: indexerName, type: streamType }, + value: unprocessedMessages?.length ?? 0, + } satisfies Message); + + parentPort?.postMessage({ + type: 'EXECUTION_DURATION', + labels: { indexer: indexerName, type: streamType }, + value: performance.now() - startTime, + } satisfies Message); + + console.log(`Success: ${indexerName}`); + } catch (err) { + await sleep(10000); + console.log(`Failed: ${indexerName}`, err); + } + } +})(); From 0c92fe2ec1f4badc6f394f7c68653b40274ccaf3 Mon Sep 17 00:00:00 2001 From: Gabe Hamilton Date: Thu, 21 Sep 2023 14:06:19 -0600 Subject: [PATCH 27/31] fix: Skip missing blocks in manual filtering (#195) Proceed with historical filtering when missing block numbers are encountered. --- .../src/historical_block_processing.rs | 20 +++++++++- ...ical_block_processing_integration_tests.rs | 22 +++++------ indexer/queryapi_coordinator/src/s3.rs | 37 ++++++++++++++++++- 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/indexer/queryapi_coordinator/src/historical_block_processing.rs b/indexer/queryapi_coordinator/src/historical_block_processing.rs index 99bee3807..ca7125d7c 100644 --- a/indexer/queryapi_coordinator/src/historical_block_processing.rs +++ b/indexer/queryapi_coordinator/src/historical_block_processing.rs @@ -330,7 +330,25 @@ async fn filter_matching_unindexed_blocks_from_lake( for current_block in (last_indexed_block + 1)..ending_block_height { // fetch block file from S3 let key = format!("{}/block.json", normalize_block_height(current_block)); - let block = s3::fetch_text_file_from_s3(&lake_bucket, key, s3_client.clone()).await?; + let s3_result = s3::fetch_text_file_from_s3(&lake_bucket, key, s3_client.clone()).await; + + if s3_result.is_err() { + let error = s3_result.err().unwrap(); + if let Some(_) = error.downcast_ref::() { + tracing::info!( + target: crate::INDEXER, + "In manual filtering, skipping block number {} which was not found. For function {:?} {:?}", + current_block, + indexer_function.account_id, + indexer_function.function_name, + ); + continue; + } else { + bail!(error); + } + } + + let block = s3_result.unwrap(); let block_view = serde_json::from_slice::< near_lake_framework::near_indexer_primitives::views::BlockView, >(block.as_ref()) diff --git a/indexer/queryapi_coordinator/src/historical_block_processing_integration_tests.rs b/indexer/queryapi_coordinator/src/historical_block_processing_integration_tests.rs index e3d8428e4..69ca67af9 100644 --- a/indexer/queryapi_coordinator/src/historical_block_processing_integration_tests.rs +++ b/indexer/queryapi_coordinator/src/historical_block_processing_integration_tests.rs @@ -17,14 +17,14 @@ mod tests { let lake_aws_access_key = env::var("LAKE_AWS_ACCESS_KEY").unwrap(); let lake_aws_secret_access_key = env::var("LAKE_AWS_SECRET_ACCESS_KEY").unwrap(); Opts { - redis_connection_string: "".to_string(), + redis_connection_string: env::var("REDIS_CONNECTION_STRING").unwrap(), lake_aws_access_key, lake_aws_secret_access_key, queue_aws_access_key: "".to_string(), queue_aws_secret_access_key: "".to_string(), aws_queue_region: "".to_string(), - queue_url: "".to_string(), - start_from_block_queue_url: "".to_string(), + queue_url: "MOCK".to_string(), + start_from_block_queue_url: "MOCK".to_string(), registry_contract_id: "".to_string(), port: 0, chain_id: ChainId::Mainnet(StartOptions::FromLatest), @@ -32,8 +32,8 @@ mod tests { } } - /// Parses env vars from .env, Run with - /// cargo test historical_block_processing_integration_tests::test_indexing_metadata_file -- mainnet from-latest; + /// Parses some env vars from .env, Run with + /// cargo test historical_block_processing_integration_tests::test_indexing_metadata_file; #[tokio::test] async fn test_indexing_metadata_file() { let opts = Opts::test_opts_with_aws(); @@ -47,8 +47,8 @@ mod tests { assert!(a.contains(&last_indexed_block)); } - /// Parses env vars from .env, Run with - /// cargo test historical_block_processing_integration_tests::test_process_historical_messages -- mainnet from-latest; + /// Parses some env vars from .env, Run with + /// cargo test historical_block_processing_integration_tests::test_process_historical_messages; #[tokio::test] async fn test_process_historical_messages() { opts::init_tracing(); @@ -93,8 +93,8 @@ mod tests { assert!(result.unwrap() > 0); } - /// Parses env vars from .env, Run with - /// cargo test historical_block_processing_integration_tests::test_filter_matching_wildcard_blocks_from_index_files -- mainnet from-latest; + /// Parses some env vars from .env, Run with + /// cargo test historical_block_processing_integration_tests::test_filter_matching_wildcard_blocks_from_index_files; #[tokio::test] async fn test_filter_matching_wildcard_blocks_from_index_files() { let contract = "*.keypom.near"; @@ -150,8 +150,8 @@ mod tests { } } - /// Parses env vars from .env, Run with - /// cargo test historical_block_processing_integration_tests::test_filter_matching_blocks_from_index_files -- mainnet from-latest; + /// Parses some env vars from .env, Run with + /// cargo test historical_block_processing_integration_tests::test_filter_matching_blocks_from_index_files; #[tokio::test] async fn test_filter_matching_blocks_from_index_files() { let contract = "*.agency.near"; diff --git a/indexer/queryapi_coordinator/src/s3.rs b/indexer/queryapi_coordinator/src/s3.rs index 6c29a06c6..f88323688 100644 --- a/indexer/queryapi_coordinator/src/s3.rs +++ b/indexer/queryapi_coordinator/src/s3.rs @@ -203,10 +203,13 @@ fn file_name_date_after(start_date: DateTime, file_name: &str) -> bool { #[cfg(test)] mod tests { - use crate::historical_block_processing::INDEXED_ACTIONS_FILES_FOLDER; use crate::historical_block_processing::INDEXED_DATA_FILES_BUCKET; + use crate::historical_block_processing::{INDEXED_ACTIONS_FILES_FOLDER, LAKE_BUCKET_PREFIX}; use crate::opts::Opts; - use crate::s3::{find_index_files_by_pattern, list_s3_bucket_by_prefix}; + use crate::s3::{ + fetch_text_file_from_s3, find_index_files_by_pattern, list_s3_bucket_by_prefix, + }; + use aws_sdk_s3::{Client as S3Client, Config}; /// Parses env vars from .env, Run with /// cargo test s3::tests::list_delta_bucket -- mainnet from-latest; @@ -305,4 +308,34 @@ mod tests { let actual = super::storage_path_for_account(account); assert_eq!(expected, actual); } + + #[tokio::test] + async fn handle_key_404() { + let mut success = false; + + let opts = Opts::test_opts_with_aws(); + let s3_config: Config = + aws_sdk_s3::config::Builder::from(&opts.lake_aws_sdk_config()).build(); + + let s3_client: S3Client = S3Client::from_conf(s3_config); + + let s3_result = fetch_text_file_from_s3( + format!("{}{}", LAKE_BUCKET_PREFIX, "mainnet").as_str(), + "does_not_exist/block.json".to_string(), + s3_client, + ) + .await; + + if s3_result.is_err() { + let wrapped_error = s3_result.err().unwrap(); + let error = wrapped_error.root_cause(); + if let Some(_) = error.downcast_ref::() { + success = true; + } else { + println!("Failed to downcast error: {:?}", error); + } + } + + assert!(success); + } } From 4682e0562283f0c9090d41a7ad7b154e061ea897 Mon Sep 17 00:00:00 2001 From: Darun Seethammagari Date: Wed, 27 Sep 2023 11:50:39 -0700 Subject: [PATCH 28/31] DPLT-1121: Generate and Support Strongly Typed Objects for DB Methods (#193) This commit adds functionality for generating TypeScript interfaces and a context object which reflect rows in each table defined in the SQL table and respective functions for insert, select, upsert, update, and delete which all take in a strongly formatted object. This allows for autocomplete and type assertions for writing objects or calling functions related to each defined table. This is accomplished by using the library [node-sql-parser](https://www.npmjs.com/package/node-sql-parser) which contains functions for converting valid Postgres DDL into an AST or list of tables. In the frontend, I have added a button which triggers code gen and attaches the types to Monaco. In addition, I set up listeners on monaco's mounting status as well as on the schemaTypes state variable which I introduced to store the generated types. These typesa re also saved to localStorage to be retrieved. The following actions trigger a code generation to take place: clicking the code gen button (Next to format button), switching to the indexingLogic.js tab, when monaco mounts, and when the schema and code are retrieved from local storage. The AST is parsed using the new PgSchemaTypeGen class. The end result is a context object formatted like so: context.db.TableName.methodName({}). NOTE: The context object added to monaco is done so as a global variable. Currently our indexers pass in a context parameter. This causes the local parameter to shadow the global one, which prevents autocomplete or type checking. In order to use the global one, the context object must be removed from the function declaration for getBlock. In the backend, the context object received a similar refactor to the context.db.TableName.methodName format. It also uses the parser library but solely to get the table names to generate the context object. --- frontend/package.json | 15 +- frontend/src/components/Editor/Editor.js | 83 +++++- .../src/components/Editor/EditorButtons.jsx | 18 +- frontend/src/utils/formatters.js | 37 ++- frontend/src/utils/formatters.test.js | 11 +- frontend/src/utils/indexerRunner.js | 89 +++--- frontend/src/utils/pgSchemaTypeGen.js | 268 ++++++++++++++++++ frontend/yarn.lock | 217 ++++++++------ runner/package.json | 1 + runner/src/dml-handler/dml-handler.test.ts | 14 +- runner/src/dml-handler/dml-handler.ts | 22 +- runner/src/indexer/indexer.test.ts | 125 ++++++-- runner/src/indexer/indexer.ts | 175 +++++++----- 13 files changed, 813 insertions(+), 262 deletions(-) create mode 100644 frontend/src/utils/pgSchemaTypeGen.js diff --git a/frontend/package.json b/frontend/package.json index ad3b59abf..521197bf3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,8 +12,8 @@ "lint": "next lint" }, "dependencies": { - "@graphiql/plugin-explorer": "0.3.0", "@graphiql/plugin-code-exporter": "0.3.0", + "@graphiql/plugin-explorer": "0.3.0", "@monaco-editor/react": "^4.1.3", "@near-lake/primitives": "0.1.0", "@next/font": "13.1.6", @@ -21,24 +21,25 @@ "@types/react": "18.0.28", "@types/react-dom": "18.0.10", "bootstrap": "^5.2.3", + "buffer": "^6.0.3", "eslint": "8.34.0", "eslint-config-next": "13.1.6", + "graphiql": "^2.4.1", "graphql": "^16.6.0", "near-api-js": "1.1.0", "near-social-bridge": "^1.4.1", "next": "13.1.6", + "node-sql-parser": "^4.10.0", "prettier": "^2.7.1", "prettier-plugin-sql": "^0.13.0", + "raw-loader": "^4.0.2", "react": "18.2.0", "react-bootstrap": "^2.7.2", + "react-bootstrap-icons": "^1.10.3", "react-dom": "18.2.0", "react-switch": "^7.0.0", + "regenerator-runtime": "^0.13.11", "styled-components": "^5.3.6", - "typescript": "4.9.5", - "graphiql": "^2.4.1", - "react-bootstrap-icons": "^1.10.3", - "buffer": "^6.0.3", - "raw-loader": "^4.0.2", - "regenerator-runtime": "^0.13.11" + "typescript": "4.9.5" } } diff --git a/frontend/src/components/Editor/Editor.js b/frontend/src/components/Editor/Editor.js index bb696bd8b..0da8175c4 100644 --- a/frontend/src/components/Editor/Editor.js +++ b/frontend/src/components/Editor/Editor.js @@ -1,10 +1,11 @@ -import React, { useEffect, useState, useMemo, useContext } from "react"; +import React, { useEffect, useState, useRef, useMemo, useContext } from "react"; import { formatSQL, formatIndexingCode, wrapCode, defaultCode, defaultSchema, + defaultSchemaTypes, } from "../../utils/formatters"; import { queryIndexerFunctionDetails } from "../../utils/queryIndexerFunction"; import { Alert } from "react-bootstrap"; @@ -20,6 +21,7 @@ import { PublishModal } from "../Modals/PublishModal"; import { ForkIndexerModal } from "../Modals/ForkIndexerModal"; import { getLatestBlockHeight } from "../../utils/getLatestBlockHeight"; import { IndexerDetailsContext } from '../../contexts/IndexerDetailsContext'; +import { PgSchemaTypeGen } from "../../utils/pgSchemaTypeGen"; const BLOCKHEIGHT_LIMIT = 3600; @@ -41,6 +43,8 @@ const Editor = ({ }#${indexerDetails.indexerName || "new"}`; const SCHEMA_STORAGE_KEY = `QueryAPI:Schema:${indexerDetails.accountId}#${indexerDetails.indexerName || "new" }`; + const SCHEMA_TYPES_STORAGE_KEY = `QueryAPI:Schema:Types:${indexerDetails.accountId}#${indexerDetails.indexerName || "new" + }`; const CODE_STORAGE_KEY = `QueryAPI:Code:${indexerDetails.accountId}#${indexerDetails.indexerName || "new" }`; @@ -53,6 +57,8 @@ const Editor = ({ const [originalIndexingCode, setOriginalIndexingCode] = useState(formatIndexingCode(defaultCode)); const [indexingCode, setIndexingCode] = useState(originalIndexingCode); const [schema, setSchema] = useState(originalSQLCode); + const [schemaTypes, setSchemaTypes] = useState(defaultSchemaTypes); + const [monacoMount, setMonacoMount] = useState(false); const [heights, setHeights] = useState(localStorage.getItem(DEBUG_LIST_STORAGE_KEY) || []); @@ -72,6 +78,9 @@ const Editor = ({ }; const indexerRunner = useMemo(() => new IndexerRunner(handleLog), []); + const pgSchemaTypeGen = new PgSchemaTypeGen(); + const disposableRef = useRef(null); + useEffect(() => { if (!indexerDetails.code || !indexerDetails.schema) return const { formattedCode, formattedSchema } = reformatAll(indexerDetails.code, indexerDetails.schema) @@ -82,10 +91,20 @@ const Editor = ({ }, [indexerDetails.code, indexerDetails.schema]); useEffect(() => { + const savedSchema = localStorage.getItem(SCHEMA_STORAGE_KEY); const savedCode = localStorage.getItem(CODE_STORAGE_KEY); - if (savedSchema) setSchema(savedSchema); + if (savedSchema) { + setSchema(savedSchema); + try { + setSchemaTypes(pgSchemaTypeGen.generateTypes(savedSchema)); + setError(() => undefined); + } catch (error) { + handleCodeGenError(error); + } + + } if (savedCode) setIndexingCode(savedCode); }, [indexerDetails.accountId, indexerDetails.indexerName]); @@ -94,25 +113,59 @@ const Editor = ({ localStorage.setItem(CODE_STORAGE_KEY, indexingCode); }, [schema, indexingCode]); + useEffect(() => { + localStorage.setItem(SCHEMA_TYPES_STORAGE_KEY, schemaTypes); + attachTypesToMonaco(); + }, [schemaTypes, monacoMount]); + const requestLatestBlockHeight = async () => { const blockHeight = getLatestBlockHeight() return blockHeight } useEffect(() => { - if (selectedTab === "playground") { - setFileName("GraphiQL"); + if (fileName === "indexingLogic.js") { + try { + setSchemaTypes(pgSchemaTypeGen.generateTypes(schema)); + setError(() => undefined); + } catch (error) { + handleCodeGenError(error); + } } - }, [selectedTab]); + }, [fileName]); useEffect(() => { localStorage.setItem(DEBUG_LIST_STORAGE_KEY, heights); }, [heights]); + const attachTypesToMonaco = () => { + // If types has been added already, dispose of them first + if (disposableRef.current) { + disposableRef.current.dispose(); + disposableRef.current = null; + } + + if (window.monaco) { // Check if monaco is loaded + // Add generated types to monaco and store disposable to clear them later + const newDisposable = monaco.languages.typescript.typescriptDefaults.addExtraLib(schemaTypes); + if (newDisposable != null) { + console.log("Types successfully imported to Editor"); + } + disposableRef.current = newDisposable; + } + } + const checkSQLSchemaFormatting = () => { try { let formatted_sql = formatSQL(schema); let formatted_schema = formatted_sql; + try { + pgSchemaTypeGen.generateTypes(formatted_sql); // Sanity check + } catch (error) { + handleCodeGenError(error); + return undefined; + } + return formatted_schema; } catch (error) { console.log("error", error); @@ -172,6 +225,7 @@ const Editor = ({ setShowResetCodeModel(false); setIndexingCode(originalIndexingCode); setSchema(originalSQLCode); + setSchemaTypes(defaultSchemaTypes); return; } @@ -179,6 +233,7 @@ const Editor = ({ if (data == null) { setIndexingCode(defaultCode); setSchema(defaultSchema); + setSchemaTypes(defaultSchemaTypes); setError(() => onLoadErrorText); } else { try { @@ -253,6 +308,22 @@ const Editor = ({ }); }; + function handleCodeGen() { + try { + setSchemaTypes(pgSchemaTypeGen.generateTypes(schema)); + attachTypesToMonaco(); // Just in case schema types have been updated but weren't added to monaco + setError(() => undefined); + } catch (error) { + handleCodeGenError(error); + } + } + + const handleCodeGenError = (error) => { + console.error("Error generating types for saved schema.\n", error); + const errorMessage = "Oh snap! We could not generate types for your SQL schema. Make sure it is proper SQL DDL." + setError(() => errorMessage); + }; + async function handleFormating() { await reformat(indexingCode, schema); } @@ -274,6 +345,7 @@ const Editor = ({ `${primitives}}`, "file:///node_modules/@near-lake/primitives/index.d.ts" ); + setMonacoMount(true); } @@ -311,6 +383,7 @@ const Editor = ({ > + + Generate Types} + > + + {(!isUserIndexer && !isCreateNewIndexer) ? ( `import {Block} from "@near-lake/primitives" /** * getBlock(block, context) applies your custom logic to a Block on Near and commits the data to a database. + * context is a global variable that contains helper methods. + * context.db is a subfield which contains helper methods to interact with your database. * * Learn more about indexers here: https://docs.near.org/concepts/advanced/indexers * - * @param {block} Block - A Near Protocol Block - * @param {context} - A set of helper methods to retrieve and commit state + * @param {block} Block - A Near Protocol Block */ -async function getBlock(block: Block, context) { +async function getBlock(block: Block) { ${code} }`; @@ -54,3 +55,33 @@ export const defaultCode = formatIndexingCode(wrapCode( export const defaultSchema = ` CREATE TABLE "indexer_storage" ("function_name" TEXT NOT NULL, "key_name" TEXT NOT NULL, "value" TEXT NOT NULL, PRIMARY KEY ("function_name", "key_name")) `; + +export const defaultSchemaTypes = `declare interface IndexerStorageItem { + function_name?: string; + key_name?: string; + value?: string; +} + +declare interface IndexerStorageInput { + function_name: string; + key_name: string; + value: string; +} + +declare const context: { + + graphql: (operation, variables) => Promise, + set: (key, value) => Promise, + log: (...log) => Promise, + fetchFromSocialApi: (path, options) => Promise, + db: { + IndexerStorage: { + insert: (objects: IndexerStorageInput | IndexerStorageInput[]) => Promise; + select: (object: IndexerStorageItem, limit = null) => Promise; + update: (whereObj: IndexerStorageItem, updateObj: IndexerStorageItem) => Promise; + upsert: (objects: IndexerStorageInput | IndexerStorageInput[], conflictColumns: IndexerStorageItem, updateColumns: IndexerStorageItem) => Promise; + delete: (object: IndexerStorageInput) => Promise; + }, + } +}; +` \ No newline at end of file diff --git a/frontend/src/utils/formatters.test.js b/frontend/src/utils/formatters.test.js index a35825e4a..258070edb 100644 --- a/frontend/src/utils/formatters.test.js +++ b/frontend/src/utils/formatters.test.js @@ -38,14 +38,15 @@ const expectedOutput3 = `import { Block } from "@near-lake/primitives"; */ /** - * getBlock(block, context) applies your custom logic to a Block on Near and commits the data to a database. - * + * getBlock(block, context) applies your custom logic to a Block on Near and commits the data to a database. + * context is a global variable that contains helper methods. + * context.db is a subfield which contains helper methods to interact with your database. + * * Learn more about indexers here: https://docs.near.org/concepts/advanced/indexers - * + * * @param {block} Block - A Near Protocol Block - * @param {context} - A set of helper methods to retrieve and commit state */ -async function getBlock(block: Block, context) { +async function getBlock(block: Block) { const h = block.header().height; console.log("About to write demo_blockheight", h); await context.set("demo_height", h); diff --git a/frontend/src/utils/indexerRunner.js b/frontend/src/utils/indexerRunner.js index 6ccc994ea..05320d3c3 100644 --- a/frontend/src/utils/indexerRunner.js +++ b/frontend/src/utils/indexerRunner.js @@ -1,6 +1,7 @@ import { Block } from "@near-lake/primitives"; import { Buffer } from "buffer"; import { fetchBlockDetails } from "./fetchBlock"; +import { PgSchemaTypeGen } from "./pgSchemaTypeGen"; global.Buffer = Buffer; export default class IndexerRunner { @@ -8,6 +9,7 @@ export default class IndexerRunner { this.handleLog = handleLog; this.currentHeight = 0; this.shouldStop = false; + this.pgSchemaTypeGen = new PgSchemaTypeGen(); } async start(startingHeight, indexingCode, schema, schemaName, option) { @@ -52,7 +54,6 @@ export default class IndexerRunner { async executeIndexerFunction(height, blockDetails, indexingCode, schema, schemaName) { let innerCode = indexingCode.match(/getBlock\s*\([^)]*\)\s*{([\s\S]*)}/)[1]; - if (blockDetails) { const block = Block.fromStreamerMessage(blockDetails); block.actions() @@ -153,57 +154,43 @@ export default class IndexerRunner { wrappedFunction(Block, streamerMessage, context); } - validateTableNames(tableNames) { - if (!(Array.isArray(tableNames) && tableNames.length > 0)) { - throw new Error("Schema does not have any tables. There should be at least one table."); - } - const correctTableNameFormat = /^[a-zA-Z_][a-zA-Z0-9_]*$/; - - tableNames.forEach(name => { - if (!name.includes("\"") && !correctTableNameFormat.test(name)) { // Only test if table name doesn't have quotes - throw new Error(`Table name ${name} is not formatted correctly. Table names must not start with a number and only contain alphanumerics or underscores.`); - } - }); - } - - getTableNames (schema) { - const tableRegex = /CREATE TABLE\s+(?:IF NOT EXISTS\s+)?"?(.+?)"?\s*\(/g; - const tableNames = Array.from(schema.matchAll(tableRegex), match => { - let tableName; - if (match[1].includes('.')) { // If expression after create has schemaName.tableName, return only tableName - tableName = match[1].split('.')[1]; - tableName = tableName.startsWith('"') ? tableName.substring(1) : tableName; - } else { - tableName = match[1]; - } - return /^\w+$/.test(tableName) ? tableName : `"${tableName}"`; // If table name has special characters, it must be inside double quotes - }); - this.validateTableNames(tableNames); - console.log('Retrieved the following table names from schema: ', tableNames); - return tableNames; - } - - sanitizeTableName (tableName) { - tableName = tableName.startsWith('"') && tableName.endsWith('"') ? tableName.substring(1, tableName.length - 1) : tableName; - return tableName.replace(/[^a-zA-Z0-9_]/g, '_'); - } - buildDatabaseContext (blockHeight, schemaName, schema) { try { - const tables = this.getTableNames(schema); + const tables = this.pgSchemaTypeGen.getTableNames(schema); + const sanitizedTableNames = new Set(); + + // Generate and collect methods for each table name const result = tables.reduce((prev, tableName) => { - const sanitizedTableName = this.sanitizeTableName(tableName); + // Generate sanitized table name and ensure no conflict + const sanitizedTableName = this.pgSchemaTypeGen.sanitizeTableName(tableName); + if (sanitizedTableNames.has(sanitizedTableName)) { + throw new Error(`Table '${tableName}' has the same name as another table in the generated types. Special characters are removed to generate context.db methods. Please rename the table.`); + } else { + sanitizedTableNames.add(sanitizedTableName); + } + + // Generate context.db methods for table const funcForTable = { - [`insert_${sanitizedTableName}`]: async (objects) => await this.dbOperationLog(blockHeight, - `Inserting object ${JSON.stringify(objects)} into table ${tableName} on schema ${schemaName}`), - [`select_${sanitizedTableName}`]: async (object, limit = 0) => await this.dbOperationLog(blockHeight, - `Selecting objects with values ${JSON.stringify(object)} from table ${tableName} on schema ${schemaName} with ${limit === 0 ? 'no' : roundedLimit.toString()} limit`), - [`update_${sanitizedTableName}`]: async (whereObj, updateObj) => await this.dbOperationLog(blockHeight, - `Updating objects that match ${JSON.stringify(whereObj)} with values ${JSON.stringify(updateObj)} in table ${tableName} on schema ${schemaName}`), - [`upsert_${sanitizedTableName}`]: async (objects, conflictColumns, updateColumns) => await this.dbOperationLog(blockHeight, - `Inserting objects with values ${JSON.stringify(objects)} in table ${tableName} on schema ${schemaName}. Conflict on columns ${conflictColumns.join(', ')} will update values in columns ${updateColumns.join(', ')}`), - [`delete_${sanitizedTableName}`]: async (object) => await this.dbOperationLog(blockHeight, - `Deleting objects with values ${JSON.stringify(object)} in table ${tableName} on schema ${schemaName}`) + [`${sanitizedTableName}`]: { + insert: async (objects) => await this.dbOperationLog(blockHeight, + `Inserting the following objects into table ${sanitizedTableName} on schema ${schemaName}`, + objects), + + select: async (object, limit = null) => await this.dbOperationLog(blockHeight, + `Selecting objects with the following values from table ${sanitizedTableName} on schema ${schemaName} with ${limit === null ? 'no' : limit} limit`, + object), + + update: async (whereObj, updateObj) => await this.dbOperationLog(blockHeight, + `Updating objects that match the specified fields with the following values in table ${sanitizedTableName} on schema ${schemaName}`, + {matchingFields: whereObj, fieldsToUpdate: updateObj}), + + upsert: async (objects, conflictColumns, updateColumns) => await this.dbOperationLog(blockHeight, + `Inserting the following objects into table ${sanitizedTableName} on schema ${schemaName}. Conflict on the specified columns will update values in the specified columns`, + {insertObjects: objects, conflictColumns: conflictColumns.join(', '), updateColumns: updateColumns.join(', ')}), + + delete: async (object) => await this.dbOperationLog(blockHeight, + `Deleting objects which match the following object's values from table ${sanitizedTableName} on schema ${schemaName}`, object) + } }; return { @@ -217,12 +204,14 @@ export default class IndexerRunner { } } - dbOperationLog(blockHeight, logMessage) { + dbOperationLog(blockHeight, logMessage, data) { this.handleLog( blockHeight, "", () => { - console.log(logMessage); + console.group(logMessage); + console.log(data); + console.groupEnd(); } ); return {}; diff --git a/frontend/src/utils/pgSchemaTypeGen.js b/frontend/src/utils/pgSchemaTypeGen.js new file mode 100644 index 000000000..8db5f2fb7 --- /dev/null +++ b/frontend/src/utils/pgSchemaTypeGen.js @@ -0,0 +1,268 @@ +import { Parser } from "node-sql-parser"; + +export class PgSchemaTypeGen { + constructor() { + this.parser = new Parser(); + this.tables = new Set(); + } + + sanitizeTableName(tableName) { + // Convert to PascalCase + let pascalCaseTableName = tableName + // Replace special characters with underscores + .replace(/[^a-zA-Z0-9_]/g, '_') + // Makes first letter and any letters following an underscore upper case + .replace(/^([a-zA-Z])|_([a-zA-Z])/g, (match) => match.toUpperCase()) + // Removes all underscores + .replace(/_/g, ''); + + // Add underscore if first character is a number + if (/^[0-9]/.test(pascalCaseTableName)) { + pascalCaseTableName = '_' + pascalCaseTableName; + } + + return pascalCaseTableName; + } + + getTableNames (schema) { + let schemaSyntaxTree = this.parser.astify(schema, { database: 'Postgresql' }); + schemaSyntaxTree = Array.isArray(schemaSyntaxTree) ? schemaSyntaxTree : [schemaSyntaxTree]; // Ensure iterable + const tableNames = new Set(); + + // Collect all table names from schema AST, throw error if duplicate table names exist + for (const statement of schemaSyntaxTree) { + if (statement.type === 'create' && statement.keyword === 'table' && statement.table !== undefined) { + const tableName = statement.table[0].table; + + if (tableNames.has(tableName)) { + throw new Error(`Table ${tableName} already exists in schema. Table names must be unique. Quotes are not allowed as a differentiator between table names.`); + } + + tableNames.add(tableName); + } + } + + // Ensure schema is not empty + if (tableNames.size === 0) { + throw new Error('Schema does not have any tables. There should be at least one table.'); + } + + const tableNamesArray = Array.from(tableNames); + return Array.from(tableNamesArray); + } + + generateTypes(sqlSchema) { + const schemaSyntaxTree = this.parser.astify(sqlSchema, { database: "Postgresql" }); + const dbSchema = {}; + + // Process each statement in the schema + for (const statement of schemaSyntaxTree) { + if (statement.type === "create" && statement.keyword === "table") { + // Process CREATE TABLE statements + const tableName = statement.table[0].table; + if (dbSchema.hasOwnProperty(tableName)) { + throw new Error(`Table ${tableName} already exists in schema. Table names must be unique. Quotes are not allowed as a differentiator between table names.`); + } + + let columns = {}; + for (const columnSpec of statement.create_definitions) { + if (columnSpec.hasOwnProperty("column") && columnSpec.hasOwnProperty("definition")) { + // New Column + this.addColumn(columnSpec, columns); + } else if (columnSpec.hasOwnProperty("constraint") && columnSpec.constraint_type == "primary key") { + // Constraint on existing column + for (const foreignKeyDef of columnSpec.definition) { + columns[foreignKeyDef.column].nullable = false; + } + } + } + dbSchema[tableName] = columns; + } else if (statement.type === "alter") { + // Process ALTER TABLE statements + const tableName = statement.table[0].table; + for (const alterSpec of statement.expr) { + switch (alterSpec.action) { + case "add": + switch (alterSpec.resource) { + case "column": // Add column to table + this.addColumn(alterSpec, dbSchema[tableName]); + break; + case "constraint": // Add constraint to column(s) (Only PRIMARY KEY constraint impacts output types) + const newConstraint = alterSpec.create_definitions; + if (newConstraint.constraint_type == "primary key") { + for (const foreignKeyDef of newConstraint.definition) { + dbSchema[tableName][foreignKeyDef.column].nullable = false; + } + } + break; + } + break; + case "drop": // Can only drop column for now + delete dbSchema[tableName][alterSpec.column.column]; + break; + } + } + } + } + + const tsTypes = this.generateTypeScriptDefinitions(dbSchema); + console.log(`Types successfully generated`); + return tsTypes; + } + + addColumn(columnDef, columns) { + const columnName = columnDef.column.column; + const columnType = this.getTypescriptType(columnDef.definition.dataType); + const nullable = this.getNullableStatus(columnDef); + const required = this.getRequiredStatus(columnDef, nullable); + if (columns.hasOwnProperty(columnName)) { + console.warn(`Column ${columnName} already exists in table. Skipping.`); + return; + } + columns[columnName] = { + type: columnType, + nullable: nullable, + required: required, + }; + } + + getNullableStatus(columnDef) { + const isPrimaryKey = + columnDef.hasOwnProperty("unique_or_primary") && + columnDef.unique_or_primary == "primary key"; + const isNullable = + columnDef.hasOwnProperty("nullable") && + columnDef.nullable.value == "not null"; + return isPrimaryKey || isNullable ? false : true; + } + + getRequiredStatus(columnDef, nullable) { + const hasDefaultValue = + columnDef.hasOwnProperty("default_val") && columnDef.default_val != null; + const isSerial = columnDef.definition.dataType + .toLowerCase() + .includes("serial"); + return hasDefaultValue || isSerial || nullable ? false : true; + } + + generateTypeScriptDefinitions(schema) { + const tableList = new Set(); + let tsDefinitions = ""; + let contextObject = `declare const context: { + graphql: (operation, variables) => Promise, + set: (key, value) => Promise, + log: (...log) => Promise, + fetchFromSocialApi: (path, options) => Promise, + db: {`; + + // Process each table + for (const [tableName, columns] of Object.entries(schema)) { + let itemDefinition = ""; + let inputDefinition = ""; + const sanitizedTableName = this.sanitizeTableName(tableName); + if (tableList.has(sanitizedTableName)) { + throw new Error(`Table '${tableName}' has the same name as another table in the generated types. Special characters are removed to generate context.db methods. Please rename the table.`); + } + tableList.add(sanitizedTableName); + // Create interfaces for strongly typed input and row item + itemDefinition += `declare interface ${sanitizedTableName}Item {\n`; + inputDefinition += `declare interface ${sanitizedTableName}Input {\n`; + for (const [columnName, columnDetails] of Object.entries(columns)) { + let tsType = columnDetails.nullable ? columnDetails.type + " | null" : columnDetails.type; + const optional = columnDetails.required ? "" : "?"; + itemDefinition += ` ${columnName}?: ${tsType};\n`; // Item fields are always optional + inputDefinition += ` ${columnName}${optional}: ${tsType};\n`; + } + itemDefinition += "}\n\n"; + inputDefinition += "}\n\n"; + + // Create type containing column names to be used as a replacement for string[]. + const columnNamesDef = `type ${sanitizedTableName}Columns = "${Object.keys(columns).join('" | "')}";\n\n`; + + // Add generated types to definitions + tsDefinitions += itemDefinition + inputDefinition + columnNamesDef; + + // Create context object with correctly formatted methods. Name, input, and output should match actual implementation + contextObject += ` + ${sanitizedTableName}: { + insert: (objectsToInsert: ${sanitizedTableName}Input | ${sanitizedTableName}Input[]) => Promise<${sanitizedTableName}Item[]>; + select: (filterObj: ${sanitizedTableName}Item, limit = null) => Promise<${sanitizedTableName}Item[]>; + update: (filterObj: ${sanitizedTableName}Item, updateObj: ${sanitizedTableName}Item) => Promise<${sanitizedTableName}Item[]>; + upsert: (objectsToInsert: ${sanitizedTableName}Input | ${sanitizedTableName}Input[], conflictColumns: ${sanitizedTableName}Columns[], updateColumns: ${sanitizedTableName}Columns[]) => Promise<${sanitizedTableName}Item[]>; + delete: (filterObj: ${sanitizedTableName}Item) => Promise<${sanitizedTableName}Item[]>; + },`; + } + + contextObject += '\n }\n};' + this.tableList = tableList; + + return tsDefinitions + contextObject; + } + + getTypescriptType(pgType) { + switch (pgType.toLowerCase()) { + // Numeric types + case "smallint": + case "integer": + case "bigint": + case "decimal": + case "numeric": + case "real": + case "double precision": + case "serial": + case "bigserial": + return "number"; + + // Monetary types + case "money": + return "number"; + + // Character types + case "character varying": + case "varchar": + case "character": + case "char": + case "text": + return "string"; + + // Binary data types + case "bytea": + return "Buffer"; + + // Boolean type + case "boolean": + return "boolean"; + + // Date/Time types + case "timestamp": + case "timestamp without time zone": + case "timestamp with time zone": + case "date": + case "time": + case "time without time zone": + case "time with time zone": + case "interval": + return "Date"; + + // UUID type + case "uuid": + return "string"; + + // JSON types + case "json": + case "jsonb": + return "any"; + + // Arrays + case "integer[]": + return "number[]"; + + case "text[]": + return "string[]"; + + // Others + default: + return "any"; + } + } +} \ No newline at end of file diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 290484472..d10e66f86 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -86,14 +86,14 @@ "@babel/runtime@^7.0.0": version "7.22.3" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.3.tgz#0a7fce51d43adbf0f7b517a71f4c3aaca92ebcbb" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.3.tgz" integrity sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ== dependencies: regenerator-runtime "^0.13.11" "@babel/runtime@^7.12.13": version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz" integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== dependencies: regenerator-runtime "^0.13.11" @@ -176,9 +176,23 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@graphiql/plugin-code-exporter@0.3.0": + version "0.3.0" + resolved "https://registry.npmjs.org/@graphiql/plugin-code-exporter/-/plugin-code-exporter-0.3.0.tgz" + integrity sha512-IQvrNJEPRPdMIG9E3nVbznGgtYYsZv5kU8LRayBqHqXoIaOsL4CTXx2PjmjJptbFuDMQxzlkYCOyW+QUcLQ2XA== + dependencies: + graphiql-code-exporter "^3.0.3" + +"@graphiql/plugin-explorer@0.3.0": + version "0.3.0" + resolved "https://registry.npmjs.org/@graphiql/plugin-explorer/-/plugin-explorer-0.3.0.tgz" + integrity sha512-ZXAfzFXuqBPzD3HNSelh23eCRE1Qdsi2ndZyOqAjcy6bxo3lSj8wrM8cN1+T1Wo+0U39SiTkdmlElZTaip7RXA== + dependencies: + graphiql-explorer "^0.9.0" + "@graphiql/react@^0.17.6": version "0.17.6" - resolved "https://registry.yarnpkg.com/@graphiql/react/-/react-0.17.6.tgz#54e3745f74ccf5cd69540aecc9dbcd15a7e28c1c" + resolved "https://registry.npmjs.org/@graphiql/react/-/react-0.17.6.tgz" integrity sha512-3k1paSRbRwVNxr2U80xnRhkws8tSErWlETJvEQBmqRcWbt0+WmwFJorkLnG1n3Wj0Ho6k4a2BAiTfJ6F4SPrLg== dependencies: "@graphiql/toolkit" "^0.8.4" @@ -198,7 +212,7 @@ "@graphiql/toolkit@^0.8.4": version "0.8.4" - resolved "https://registry.yarnpkg.com/@graphiql/toolkit/-/toolkit-0.8.4.tgz#8b697d140a3e96a6702428cbb8da4e8eb29162b3" + resolved "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.8.4.tgz" integrity sha512-cFUGqh3Dau+SD3Vq9EFlZrhzYfaHKyOJveFtaCR+U5Cn/S68p7oy+vQBIdwtO6J2J58FncnwBbVRfr+IvVfZqQ== dependencies: "@n1ru4l/push-pull-async-iterable-iterator" "^3.1.0" @@ -272,12 +286,12 @@ "@n1ru4l/push-pull-async-iterable-iterator@^3.1.0": version "3.2.0" - resolved "https://registry.yarnpkg.com/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-3.2.0.tgz#c15791112db68dd9315d329d652b7e797f737655" + resolved "https://registry.npmjs.org/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-3.2.0.tgz" integrity sha512-3fkKj25kEjsfObL6IlKPAlHYPq/oYwUkkQ03zsTTiDjD7vg/RxjdiLeCydqtxHZP0JgsXL3D/X5oAkMGzuUp/Q== "@near-lake/primitives@0.1.0": version "0.1.0" - resolved "https://registry.yarnpkg.com/@near-lake/primitives/-/primitives-0.1.0.tgz#c9dc196cad82b668e773eab7f673edfc6a877cea" + resolved "https://registry.npmjs.org/@near-lake/primitives/-/primitives-0.1.0.tgz" integrity sha512-SvL6mA0SsqAz5AC2811I+cI9Mpayax8VsoRbY0Bizk5eYiGCT1u1iBBa8f1nikquDfJCEK+sBCt751Nz/xoZjw== "@next/env@13.1.6": @@ -309,12 +323,12 @@ "@next/swc-darwin-arm64@13.1.6": version "13.1.6" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.1.6.tgz#ec1b90fd9bf809d8b81004c5182e254dced4ad96" + resolved "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.1.6.tgz" integrity sha512-KKRQH4DDE4kONXCvFMNBZGDb499Hs+xcFAwvj+rfSUssIDrZOlyfJNy55rH5t2Qxed1e4K80KEJgsxKQN1/fyw== "@next/swc-darwin-x64@13.1.6": version "13.1.6" - resolved "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.1.6.tgz" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.1.6.tgz#e869ac75d16995eee733a7d1550322d9051c1eb4" integrity sha512-/uOky5PaZDoaU99ohjtNcDTJ6ks/gZ5ykTQDvNZDjIoCxFe3+t06bxsTPY6tAO6uEAw5f6vVFX5H5KLwhrkZCA== "@next/swc-freebsd-x64@13.1.6": @@ -402,7 +416,7 @@ "@reach/auto-id@0.17.0": version "0.17.0" - resolved "https://registry.yarnpkg.com/@reach/auto-id/-/auto-id-0.17.0.tgz#60cce65eb7a0d6de605820727f00dfe2b03b5f17" + resolved "https://registry.npmjs.org/@reach/auto-id/-/auto-id-0.17.0.tgz" integrity sha512-ud8iPwF52RVzEmkHq1twuqGuPA+moreumUHdtgvU3sr3/15BNhwp3KyDLrKKSz0LP1r3V4pSdyF9MbYM8BoSjA== dependencies: "@reach/utils" "0.17.0" @@ -410,7 +424,7 @@ "@reach/combobox@^0.17.0": version "0.17.0" - resolved "https://registry.yarnpkg.com/@reach/combobox/-/combobox-0.17.0.tgz#fb9d71d2d5aff3b339dce0ec5e3b73628a51b009" + resolved "https://registry.npmjs.org/@reach/combobox/-/combobox-0.17.0.tgz" integrity sha512-2mYvU5agOBCQBMdlM4cri+P1BbNwp05P1OuDyc33xJSNiBG7BMy4+ZSHJ0X4fyle6rHwSgCAOCLOeWV1XUYjoQ== dependencies: "@reach/auto-id" "0.17.0" @@ -424,7 +438,7 @@ "@reach/descendants@0.17.0": version "0.17.0" - resolved "https://registry.yarnpkg.com/@reach/descendants/-/descendants-0.17.0.tgz#3fb087125a67870acd4dee1528449ed546829b67" + resolved "https://registry.npmjs.org/@reach/descendants/-/descendants-0.17.0.tgz" integrity sha512-c7lUaBfjgcmKFZiAWqhG+VnXDMEhPkI4kAav/82XKZD6NVvFjsQOTH+v3tUkskrAPV44Yuch0mFW/u5Ntifr7Q== dependencies: "@reach/utils" "0.17.0" @@ -432,7 +446,7 @@ "@reach/dialog@^0.17.0": version "0.17.0" - resolved "https://registry.yarnpkg.com/@reach/dialog/-/dialog-0.17.0.tgz#81c48dd4405945dfc6b6c3e5e125db2c4324e9e8" + resolved "https://registry.npmjs.org/@reach/dialog/-/dialog-0.17.0.tgz" integrity sha512-AnfKXugqDTGbeG3c8xDcrQDE4h9b/vnc27Sa118oQSquz52fneUeX9MeFb5ZEiBJK8T5NJpv7QUTBIKnFCAH5A== dependencies: "@reach/portal" "0.17.0" @@ -444,7 +458,7 @@ "@reach/dropdown@0.17.0": version "0.17.0" - resolved "https://registry.yarnpkg.com/@reach/dropdown/-/dropdown-0.17.0.tgz#8140bb2e6a045f91e07c6d5a6ff960958df2ef33" + resolved "https://registry.npmjs.org/@reach/dropdown/-/dropdown-0.17.0.tgz" integrity sha512-qBTIGInhxtPHtdj4Pl2XZgZMz3e37liydh0xR3qc48syu7g71sL4nqyKjOzThykyfhA3Pb3/wFgsFJKGTSdaig== dependencies: "@reach/auto-id" "0.17.0" @@ -455,7 +469,7 @@ "@reach/listbox@^0.17.0": version "0.17.0" - resolved "https://registry.yarnpkg.com/@reach/listbox/-/listbox-0.17.0.tgz#e709f31056bb77781e74c9f0b69bf9ec8efbbc8b" + resolved "https://registry.npmjs.org/@reach/listbox/-/listbox-0.17.0.tgz" integrity sha512-AMnH1P6/3VKy2V/nPb4Es441arYR+t4YRdh9jdcFVrCOD6y7CQrlmxsYjeg9Ocdz08XpdoEBHM3PKLJqNAUr7A== dependencies: "@reach/auto-id" "0.17.0" @@ -467,7 +481,7 @@ "@reach/machine@0.17.0": version "0.17.0" - resolved "https://registry.yarnpkg.com/@reach/machine/-/machine-0.17.0.tgz#4e4bbf66e3c3934e65243485ac84f6f8fa3d9a24" + resolved "https://registry.npmjs.org/@reach/machine/-/machine-0.17.0.tgz" integrity sha512-9EHnuPgXzkbRENvRUzJvVvYt+C2jp7PGN0xon7ffmKoK8rTO6eA/bb7P0xgloyDDQtu88TBUXKzW0uASqhTXGA== dependencies: "@reach/utils" "0.17.0" @@ -476,7 +490,7 @@ "@reach/menu-button@^0.17.0": version "0.17.0" - resolved "https://registry.yarnpkg.com/@reach/menu-button/-/menu-button-0.17.0.tgz#9f40979129b61f8bdc19590c527f7ed4883d2dce" + resolved "https://registry.npmjs.org/@reach/menu-button/-/menu-button-0.17.0.tgz" integrity sha512-YyuYVyMZKamPtivoEI6D0UEILYH3qZtg4kJzEAuzPmoR/aHN66NZO75Fx0gtjG1S6fZfbiARaCOZJC0VEiDOtQ== dependencies: "@reach/dropdown" "0.17.0" @@ -488,12 +502,12 @@ "@reach/observe-rect@1.2.0": version "1.2.0" - resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2" + resolved "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.2.0.tgz" integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ== "@reach/popover@0.17.0": version "0.17.0" - resolved "https://registry.yarnpkg.com/@reach/popover/-/popover-0.17.0.tgz#feda6961f37d17b8738d2d52af6bfc5c4584464f" + resolved "https://registry.npmjs.org/@reach/popover/-/popover-0.17.0.tgz" integrity sha512-yYbBF4fMz4Ml4LB3agobZjcZ/oPtPsNv70ZAd7lEC2h7cvhF453pA+zOBGYTPGupKaeBvgAnrMjj7RnxDU5hoQ== dependencies: "@reach/portal" "0.17.0" @@ -504,7 +518,7 @@ "@reach/portal@0.17.0": version "0.17.0" - resolved "https://registry.yarnpkg.com/@reach/portal/-/portal-0.17.0.tgz#1dd69ffc8ffc8ba3e26dd127bf1cc4b15f0c6bdc" + resolved "https://registry.npmjs.org/@reach/portal/-/portal-0.17.0.tgz" integrity sha512-+IxsgVycOj+WOeNPL2NdgooUdHPSY285wCtj/iWID6akyr4FgGUK7sMhRM9aGFyrGpx2vzr+eggbUmAVZwOz+A== dependencies: "@reach/utils" "0.17.0" @@ -513,7 +527,7 @@ "@reach/rect@0.17.0": version "0.17.0" - resolved "https://registry.yarnpkg.com/@reach/rect/-/rect-0.17.0.tgz#804f0cfb211e0beb81632c64d4532ec9d1d73c48" + resolved "https://registry.npmjs.org/@reach/rect/-/rect-0.17.0.tgz" integrity sha512-3YB7KA5cLjbLc20bmPkJ06DIfXSK06Cb5BbD2dHgKXjUkT9WjZaLYIbYCO8dVjwcyO3GCNfOmPxy62VsPmZwYA== dependencies: "@reach/observe-rect" "1.2.0" @@ -524,7 +538,7 @@ "@reach/tooltip@^0.17.0": version "0.17.0" - resolved "https://registry.yarnpkg.com/@reach/tooltip/-/tooltip-0.17.0.tgz#044b43de248a05b18641b4220310983cb54675a2" + resolved "https://registry.npmjs.org/@reach/tooltip/-/tooltip-0.17.0.tgz" integrity sha512-HP8Blordzqb/Cxg+jnhGmWQfKgypamcYLBPlcx6jconyV5iLJ5m93qipr1giK7MqKT2wlsKWy44ZcOrJ+Wrf8w== dependencies: "@reach/auto-id" "0.17.0" @@ -538,7 +552,7 @@ "@reach/utils@0.17.0": version "0.17.0" - resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.17.0.tgz#3d1d2ec56d857f04fe092710d8faee2b2b121303" + resolved "https://registry.npmjs.org/@reach/utils/-/utils-0.17.0.tgz" integrity sha512-M5y8fCBbrWeIsxedgcSw6oDlAMQDkl5uv3VnMVJ7guwpf4E48Xlh1v66z/1BgN/WYe2y8mB/ilFD2nysEfdGeA== dependencies: tiny-warning "^1.0.3" @@ -546,7 +560,7 @@ "@reach/visually-hidden@0.17.0", "@reach/visually-hidden@^0.17.0": version "0.17.0" - resolved "https://registry.yarnpkg.com/@reach/visually-hidden/-/visually-hidden-0.17.0.tgz#033adba10b5ec419649da8d6bd8e46db06d4c3a1" + resolved "https://registry.npmjs.org/@reach/visually-hidden/-/visually-hidden-0.17.0.tgz" integrity sha512-T6xF3Nv8vVnjVkGU6cm0+kWtvliLqPAo8PcZ+WxkKacZsaHTjaZb4v1PaCcyQHmuTNT/vtTVNOJLG0SjQOIb7g== dependencies: prop-types "^15.7.2" @@ -594,9 +608,9 @@ tslib "^2.4.0" "@types/json-schema@^7.0.8": - version "7.0.11" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" - integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + version "7.0.12" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz" + integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== "@types/json5@^0.0.29": version "0.0.29" @@ -692,7 +706,7 @@ "@xstate/fsm@1.4.0": version "1.4.0" - resolved "https://registry.yarnpkg.com/@xstate/fsm/-/fsm-1.4.0.tgz#6fd082336fde4d026e9e448576189ee5265fa51a" + resolved "https://registry.npmjs.org/@xstate/fsm/-/fsm-1.4.0.tgz" integrity sha512-uTHDeu2xI5E1IFwf37JFQM31RrH7mY7877RqPBS4ZqSNUwoLDuct8AhBWaXGnVizBAYyimVwgCyGa9z/NiRhXA== acorn-jsx@^5.3.2: @@ -707,7 +721,7 @@ acorn@^8.8.0: ajv-keywords@^3.5.2: version "3.5.2" - resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: @@ -850,17 +864,17 @@ base-x@^3.0.2: base64-js@^1.3.1: version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== big-integer@^1.6.48: version "1.6.51" - resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" + resolved "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz" integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== big.js@^5.2.2: version "5.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + resolved "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz" integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== bn.js@5.2.1, bn.js@^5.2.0: @@ -906,7 +920,7 @@ bs58@^4.0.0: buffer@^6.0.3: version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== dependencies: base64-js "^1.3.1" @@ -931,9 +945,9 @@ camelize@^1.0.0: integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== caniuse-lite@^1.0.30001406: - version "1.0.30001451" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001451.tgz" - integrity sha512-XY7UbUpGRatZzoRft//5xOa69/1iGJRBlrieH6QYrkKLIFn3m7OVEJ81dSrKoy2BnKsdbX5cLrOispZNYo9v2w== + version "1.0.30001527" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001527.tgz" + integrity sha512-YkJi7RwPgWtXVSgK4lG9AHH57nSzvvOp9MesgXmw4Q7n0C3H04L0foHqfxcmSAm5AcWb8dW9AYj2tR7/5GnddQ== capability@^0.2.5: version "0.2.5" @@ -969,19 +983,19 @@ client-only@0.0.1: clsx@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" + resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== codemirror-graphql@^2.0.8: version "2.0.8" - resolved "https://registry.yarnpkg.com/codemirror-graphql/-/codemirror-graphql-2.0.8.tgz#0d63cc6a5c5792081041e319078cfc2969dd97ef" + resolved "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-2.0.8.tgz" integrity sha512-EU+pXsSKZJAFVdF8j5hbB5gqXsDDjsBiJoohQq09yhsr69pzaI8ZrXjmpuR4CMyf9jgqcz5KK7rsTmxDHmeJPQ== dependencies: graphql-language-service "5.1.6" codemirror@^5.65.3: version "5.65.13" - resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.13.tgz#c098a6f409db8b5a7c5722788bd9fa3bb2367f2e" + resolved "https://registry.npmjs.org/codemirror/-/codemirror-5.65.13.tgz" integrity sha512-SVWEzKXmbHmTQQWaz03Shrh4nybG0wXx2MEu3FO4ezbPW8IbnZEd5iGHGEffSUaitKYa3i+pHpBsSvw8sPHtzg== color-convert@^1.9.0: @@ -1010,7 +1024,7 @@ color-name@~1.1.4: commander@^2.19.0: version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== concat-map@0.0.1: @@ -1018,9 +1032,9 @@ concat-map@0.0.1: resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -copy-to-clipboard@^3.2.0: +copy-to-clipboard@^3.0.8, copy-to-clipboard@^3.2.0: version "3.3.3" - resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0" + resolved "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz" integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA== dependencies: toggle-selection "^1.0.6" @@ -1130,7 +1144,7 @@ dequal@^2.0.2, dequal@^2.0.3: detect-node-es@^1.1.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" + resolved "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz" integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== dir-glob@^3.0.1: @@ -1142,7 +1156,7 @@ dir-glob@^3.0.1: discontinuous-range@1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" + resolved "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz" integrity sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ== doctrine@^2.1.0: @@ -1174,20 +1188,20 @@ emoji-regex@^9.2.2: emojis-list@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + resolved "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz" integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== enhanced-resolve@^5.10.0: - version "5.12.0" - resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz" - integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ== + version "5.15.0" + resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz" + integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== dependencies: graceful-fs "^4.2.4" tapable "^2.2.0" entities@~2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" + resolved "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz" integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== error-polyfill@^0.1.3: @@ -1574,7 +1588,7 @@ flatted@^3.1.0: focus-lock@^0.11.6: version "0.11.6" - resolved "https://registry.yarnpkg.com/focus-lock/-/focus-lock-0.11.6.tgz#e8821e21d218f03e100f7dc27b733f9c4f61e683" + resolved "https://registry.npmjs.org/focus-lock/-/focus-lock-0.11.6.tgz" integrity sha512-KSuV3ur4gf2KqMNoZx3nXNVhqCkn42GuTYCX4tXPEwf0MjpFQmNMiN6m7dXaUXgIoivL6/65agoUMg4RLS0Vbg== dependencies: tslib "^2.0.3" @@ -1622,7 +1636,7 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@ get-nonce@^1.0.0: version "1.0.1" - resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" + resolved "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz" integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== get-symbol-description@^1.0.0: @@ -1733,9 +1747,21 @@ grapheme-splitter@^1.0.4: resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +graphiql-code-exporter@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/graphiql-code-exporter/-/graphiql-code-exporter-3.0.3.tgz" + integrity sha512-Ml3J/ojCQ56qrIgJPDCrWQ2cpI/6yio2P1tHPBuvhGJ2zVSUCH/D+v1DIwXIzsAMwqq0WkaknqH3iuA6LD5A5A== + dependencies: + copy-to-clipboard "^3.0.8" + +graphiql-explorer@^0.9.0: + version "0.9.0" + resolved "https://registry.npmjs.org/graphiql-explorer/-/graphiql-explorer-0.9.0.tgz" + integrity sha512-fZC/wsuatqiQDO2otchxriFO0LaWIo/ovF/CQJ1yOudmY0P7pzDiP+l9CEHUiWbizk3e99x6DQG4XG1VxA+d6A== + graphiql@^2.4.1: version "2.4.7" - resolved "https://registry.yarnpkg.com/graphiql/-/graphiql-2.4.7.tgz#77eae9e8b31628bad363384c5b382de9fad1ff86" + resolved "https://registry.npmjs.org/graphiql/-/graphiql-2.4.7.tgz" integrity sha512-Fm3fVI65EPyXy+PdbeQUyODTwl2NhpZ47msGnGwpDvdEzYdgF7pPrxL96xCfF31KIauS4+ceEJ+ZwEe5iLWiQw== dependencies: "@graphiql/react" "^0.17.6" @@ -1745,7 +1771,7 @@ graphiql@^2.4.1: graphql-language-service@5.1.6, graphql-language-service@^5.1.6: version "5.1.6" - resolved "https://registry.yarnpkg.com/graphql-language-service/-/graphql-language-service-5.1.6.tgz#0d6d2b2bb09cf0d02c82fd97628dfdc63ce7936b" + resolved "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-5.1.6.tgz" integrity sha512-sl9HTlE/sBoFvZ2SPGnApwpp/a4ahl1d49SOxGm2OIYOslFv00MK7AYms9Yx91omOwAp74is10S7Cjamh5TRQw== dependencies: nullthrows "^1.0.0" @@ -1822,7 +1848,7 @@ http-errors@^1.7.2: ieee754@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== ignore@^5.2.0: @@ -1969,14 +1995,14 @@ is-path-inside@^3.0.3: is-plain-object@^2.0.4: version "2.0.4" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz" integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== dependencies: isobject "^3.0.1" is-primitive@^3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-3.0.1.tgz#98c4db1abff185485a657fc2905052b940524d05" + resolved "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz" integrity sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w== is-regex@^1.1.4: @@ -2063,7 +2089,7 @@ isexe@^2.0.0: isobject@^3.0.1: version "3.0.1" - resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== js-sdsl@^4.1.4: @@ -2112,7 +2138,7 @@ json5@^1.0.1: json5@^2.1.2: version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== "jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3: @@ -2145,14 +2171,14 @@ levn@^0.4.1: linkify-it@^3.0.1: version "3.0.3" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e" + resolved "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz" integrity sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ== dependencies: uc.micro "^1.0.1" loader-utils@^2.0.0: version "2.0.4" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" + resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz" integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== dependencies: big.js "^5.2.2" @@ -2192,7 +2218,7 @@ lru-cache@^6.0.0: markdown-it@^12.2.0: version "12.3.2" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90" + resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz" integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg== dependencies: argparse "^2.0.1" @@ -2203,7 +2229,7 @@ markdown-it@^12.2.0: mdurl@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + resolved "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz" integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== merge2@^1.3.0, merge2@^1.4.1: @@ -2213,7 +2239,7 @@ merge2@^1.3.0, merge2@^1.4.1: meros@^1.1.4: version "1.3.0" - resolved "https://registry.yarnpkg.com/meros/-/meros-1.3.0.tgz#c617d2092739d55286bf618129280f362e6242f2" + resolved "https://registry.npmjs.org/meros/-/meros-1.3.0.tgz" integrity sha512-2BNGOimxEz5hmjUG2FwoxCt5HN7BXdaWyFqEwxPTrJzVdABtrL4TiHTcsWSFAxPQ/tOnEaQEJh3qWq71QRMY+w== micromatch@^4.0.4: @@ -2238,7 +2264,7 @@ minimist@^1.2.0, minimist@^1.2.6: moo@^0.5.0: version "0.5.2" - resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.2.tgz#f9fe82473bc7c184b0d32e2215d3f6e67278733c" + resolved "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz" integrity sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q== ms@2.1.2, ms@^2.1.1: @@ -2280,12 +2306,12 @@ near-api-js@1.1.0: near-social-bridge@^1.4.1: version "1.4.1" - resolved "https://registry.yarnpkg.com/near-social-bridge/-/near-social-bridge-1.4.1.tgz#fa6757a95a0de5007bb0ebf28d46810c1a255bac" + resolved "https://registry.npmjs.org/near-social-bridge/-/near-social-bridge-1.4.1.tgz" integrity sha512-e8hTbBI9Pwq1aKpM6kvZ3Sy1DjjcAOau0wF/Sp9LsT7q2+F1QxeDMkO7ndQrkJI7K0+CIAvjFmoex0ikxBtnug== nearley@^2.20.1: version "2.20.1" - resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.20.1.tgz#246cd33eff0d012faf197ff6774d7ac78acdd474" + resolved "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz" integrity sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ== dependencies: commander "^2.19.0" @@ -2325,16 +2351,23 @@ node-fetch@^2.6.1: dependencies: whatwg-url "^5.0.0" +node-sql-parser@^4.10.0: + version "4.10.0" + resolved "https://registry.yarnpkg.com/node-sql-parser/-/node-sql-parser-4.10.0.tgz#a53d7a7570ad62ffccfead4391b40ca09dc996e8" + integrity sha512-P4LZNX8drf+0X5zPtcE5o1SV7Wn4VpTGSYOnN8uY+TswtHrg3ymb193tYpF8EMp2LhGqqDUqTAnCr8hqjN3uQw== + dependencies: + big-integer "^1.6.48" + node-sql-parser@^4.4.0: version "4.6.6" - resolved "https://registry.yarnpkg.com/node-sql-parser/-/node-sql-parser-4.6.6.tgz#910fcd4ba0132d9a5a8c312637313acc2b43b24a" + resolved "https://registry.npmjs.org/node-sql-parser/-/node-sql-parser-4.6.6.tgz" integrity sha512-zpash5xnRY6+0C9HFru32iRJV1LTkwtrVpO90i385tYVF6efyXK/B3Nsq/15Fuv2utxrqHNjKtL55OHb8sl+eQ== dependencies: big-integer "^1.6.48" nullthrows@^1.0.0: version "1.1.1" - resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" + resolved "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz" integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw== o3@^1.0.3: @@ -2517,7 +2550,7 @@ prelude-ls@^1.2.1: prettier-plugin-sql@^0.13.0: version "0.13.0" - resolved "https://registry.yarnpkg.com/prettier-plugin-sql/-/prettier-plugin-sql-0.13.0.tgz#047b395ca1c04b332772550ca69aa83121802c37" + resolved "https://registry.npmjs.org/prettier-plugin-sql/-/prettier-plugin-sql-0.13.0.tgz" integrity sha512-Ui9603tDD6PFyr7JvIEoE6cIFMQnJVDriG+oLyVThsGo/MIl5ek18JhH3xtox9ux8jvyww/FUFrJzxpZ7FIdvw== dependencies: node-sql-parser "^4.4.0" @@ -2558,12 +2591,12 @@ queue-microtask@^1.2.2: railroad-diagrams@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" + resolved "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz" integrity sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A== randexp@0.4.6: version "0.4.6" - resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" + resolved "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz" integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ== dependencies: discontinuous-range "1.0.0" @@ -2571,7 +2604,7 @@ randexp@0.4.6: raw-loader@^4.0.2: version "4.0.2" - resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6" + resolved "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz" integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA== dependencies: loader-utils "^2.0.0" @@ -2579,7 +2612,7 @@ raw-loader@^4.0.2: react-bootstrap-icons@^1.10.3: version "1.10.3" - resolved "https://registry.yarnpkg.com/react-bootstrap-icons/-/react-bootstrap-icons-1.10.3.tgz#5d64a93c7b172856b03c7d3cd5119a025b094966" + resolved "https://registry.npmjs.org/react-bootstrap-icons/-/react-bootstrap-icons-1.10.3.tgz" integrity sha512-j4hSby6gT9/enhl3ybB1tfr1slZNAYXDVntcRrmVjxB3//2WwqrzpESVqKhyayYVaWpEtnwf9wgUQ03cuziwrw== dependencies: prop-types "^15.7.2" @@ -2604,7 +2637,7 @@ react-bootstrap@^2.7.2: react-clientside-effect@^1.2.6: version "1.2.6" - resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz#29f9b14e944a376b03fb650eed2a754dd128ea3a" + resolved "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz" integrity sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg== dependencies: "@babel/runtime" "^7.12.13" @@ -2619,7 +2652,7 @@ react-dom@18.2.0: react-focus-lock@^2.5.2: version "2.9.4" - resolved "https://registry.yarnpkg.com/react-focus-lock/-/react-focus-lock-2.9.4.tgz#4753f6dcd167c39050c9d84f9c63c71b3ff8462e" + resolved "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.9.4.tgz" integrity sha512-7pEdXyMseqm3kVjhdVH18sovparAzLg5h6WvIx7/Ck3ekjhrrDMEegHSa3swwC8wgfdd7DIdUVRGeiHT9/7Sgg== dependencies: "@babel/runtime" "^7.0.0" @@ -2641,7 +2674,7 @@ react-lifecycles-compat@^3.0.4: react-remove-scroll-bar@^2.3.4: version "2.3.4" - resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz#53e272d7a5cb8242990c7f144c44d8bd8ab5afd9" + resolved "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz" integrity sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A== dependencies: react-style-singleton "^2.2.1" @@ -2649,7 +2682,7 @@ react-remove-scroll-bar@^2.3.4: react-remove-scroll@^2.4.3: version "2.5.6" - resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.6.tgz#7510b8079e9c7eebe00e65a33daaa3aa29a10336" + resolved "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.6.tgz" integrity sha512-bO856ad1uDYLefgArk559IzUNeQ6SWH4QnrevIUjH+GczV56giDfl3h0Idptf2oIKxQmd1p9BN25jleKodTALg== dependencies: react-remove-scroll-bar "^2.3.4" @@ -2660,7 +2693,7 @@ react-remove-scroll@^2.4.3: react-style-singleton@^2.2.1: version "2.2.1" - resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" + resolved "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz" integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g== dependencies: get-nonce "^1.0.0" @@ -2669,7 +2702,7 @@ react-style-singleton@^2.2.1: react-switch@^7.0.0: version "7.0.0" - resolved "https://registry.yarnpkg.com/react-switch/-/react-switch-7.0.0.tgz#400990bb9822864938e343ed24f13276a617bdc0" + resolved "https://registry.npmjs.org/react-switch/-/react-switch-7.0.0.tgz" integrity sha512-KkDeW+cozZXI6knDPyUt3KBN1rmhoVYgAdCJqAh7st7tk8YE6N0iR89zjCWO8T8dUTeJGTR0KU+5CHCRMRffiA== dependencies: prop-types "^15.7.2" @@ -2735,7 +2768,7 @@ resolve@^2.0.0-next.4: ret@~0.1.10: version "0.1.15" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + resolved "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== reusify@^1.0.4: @@ -2779,9 +2812,9 @@ scheduler@^0.23.0: loose-envify "^1.1.0" schema-utils@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" - integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== + version "3.3.0" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== dependencies: "@types/json-schema" "^7.0.8" ajv "^6.12.5" @@ -2801,7 +2834,7 @@ semver@^7.3.7: set-value@^4.1.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-4.1.0.tgz#aa433662d87081b75ad88a4743bd450f044e7d09" + resolved "https://registry.npmjs.org/set-value/-/set-value-4.1.0.tgz" integrity sha512-zTEg4HL0RwVrqcWs3ztF+x1vkxfm0lP+MQQFPiMJTKVceBwEV0A569Ou8l9IYQG8jOZdMVI1hGsc0tmeD2o/Lw== dependencies: is-plain-object "^2.0.4" @@ -2855,7 +2888,7 @@ source-map-js@^1.0.2: sql-formatter@^11.0.2: version "11.0.2" - resolved "https://registry.yarnpkg.com/sql-formatter/-/sql-formatter-11.0.2.tgz#2fde373c8c1845f8ee9f201d2eccb1fb365cd893" + resolved "https://registry.npmjs.org/sql-formatter/-/sql-formatter-11.0.2.tgz" integrity sha512-6QumAdGHEnI5dXEq1d0aBRP876AyA9Wp/UE7wopKNA2Mp9sKGRKVqGgoWHk4dr0J0nceesC85Y0p36qmGoNqhw== dependencies: argparse "^2.0.1" @@ -2979,7 +3012,7 @@ synckit@^0.8.4: tabbable@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-4.0.0.tgz#5bff1d1135df1482cf0f0206434f15eadbeb9261" + resolved "https://registry.npmjs.org/tabbable/-/tabbable-4.0.0.tgz" integrity sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ== tapable@^2.2.0: @@ -3007,7 +3040,7 @@ tiny-glob@^0.2.9: tiny-warning@^1.0.3: version "1.0.3" - resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" + resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== to-fast-properties@^2.0.0: @@ -3024,7 +3057,7 @@ to-regex-range@^5.0.1: toggle-selection@^1.0.6: version "1.0.6" - resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" + resolved "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz" integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ== toidentifier@1.0.1: @@ -3059,7 +3092,7 @@ tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.5.0: tslib@^2.0.3, tslib@^2.3.0: version "2.5.2" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.2.tgz#1b6f07185c881557b0ffa84b111a0106989e8338" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz" integrity sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA== tsutils@^3.21.0: @@ -3107,7 +3140,7 @@ u3@^0.1.1: uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" - resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" + resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz" integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== unbox-primitive@^1.0.2: @@ -3139,14 +3172,14 @@ uri-js@^4.2.2: use-callback-ref@^1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.0.tgz#772199899b9c9a50526fedc4993fc7fa1f7e32d5" + resolved "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.0.tgz" integrity sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w== dependencies: tslib "^2.0.0" use-sidecar@^1.1.2: version "1.1.2" - resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" + resolved "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz" integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw== dependencies: detect-node-es "^1.1.0" @@ -3154,7 +3187,7 @@ use-sidecar@^1.1.2: vscode-languageserver-types@^3.17.1: version "3.17.3" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz#72d05e47b73be93acb84d6e311b5786390f13f64" + resolved "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz" integrity sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA== warning@^4.0.0, warning@^4.0.3: diff --git a/runner/package.json b/runner/package.json index 274e2553c..6d14e3e5c 100644 --- a/runner/package.json +++ b/runner/package.json @@ -46,6 +46,7 @@ "aws-sdk": "^2.1402.0", "express": "^4.18.2", "node-fetch": "^2.6.11", + "node-sql-parser": "^4.10.0", "pg": "^8.11.1", "pg-format": "^1.0.4", "pluralize": "^8.0.0", diff --git a/runner/src/dml-handler/dml-handler.test.ts b/runner/src/dml-handler/dml-handler.test.ts index 73f5a945a..ab340f7f8 100644 --- a/runner/src/dml-handler/dml-handler.test.ts +++ b/runner/src/dml-handler/dml-handler.test.ts @@ -39,7 +39,7 @@ describe('DML Handler tests', () => { await dmlHandler.insert(SCHEMA, TABLE_NAME, [inputObj]); expect(query.mock.calls).toEqual([ - ['INSERT INTO test_schema.test_table (account_id, block_height, block_timestamp, content, receipt_id, accounts_liked) VALUES (\'test_acc_near\', \'999\', \'UTC\', \'test_content\', \'111\', \'["cwpuzzles.near","devbose.near"]\') RETURNING *', []] + ['INSERT INTO test_schema."test_table" (account_id, block_height, block_timestamp, content, receipt_id, accounts_liked) VALUES (\'test_acc_near\', \'999\', \'UTC\', \'test_content\', \'111\', \'["cwpuzzles.near","devbose.near"]\') RETURNING *', []] ]); }); @@ -59,7 +59,7 @@ describe('DML Handler tests', () => { await dmlHandler.insert(SCHEMA, TABLE_NAME, inputObj); expect(query.mock.calls).toEqual([ - ['INSERT INTO test_schema.test_table (account_id, block_height, receipt_id) VALUES (\'morgs_near\', \'1\', \'abc\'), (\'morgs_near\', \'2\', \'abc\') RETURNING *', []] + ['INSERT INTO test_schema."test_table" (account_id, block_height, receipt_id) VALUES (\'morgs_near\', \'1\', \'abc\'), (\'morgs_near\', \'2\', \'abc\') RETURNING *', []] ]); }); @@ -73,7 +73,7 @@ describe('DML Handler tests', () => { await dmlHandler.select(SCHEMA, TABLE_NAME, inputObj); expect(query.mock.calls).toEqual([ - ['SELECT * FROM test_schema.test_table WHERE account_id=$1 AND block_height=$2', Object.values(inputObj)] + ['SELECT * FROM test_schema."test_table" WHERE account_id=$1 AND block_height=$2', Object.values(inputObj)] ]); }); @@ -87,7 +87,7 @@ describe('DML Handler tests', () => { await dmlHandler.select(SCHEMA, TABLE_NAME, inputObj, 1); expect(query.mock.calls).toEqual([ - ['SELECT * FROM test_schema.test_table WHERE account_id=$1 AND block_height=$2 LIMIT 1', Object.values(inputObj)] + ['SELECT * FROM test_schema."test_table" WHERE account_id=$1 AND block_height=$2 LIMIT 1', Object.values(inputObj)] ]); }); @@ -106,7 +106,7 @@ describe('DML Handler tests', () => { await dmlHandler.update(SCHEMA, TABLE_NAME, whereObj, updateObj); expect(query.mock.calls).toEqual([ - ['UPDATE test_schema.test_table SET content=$1, receipt_id=$2 WHERE account_id=$3 AND block_height=$4 RETURNING *', [...Object.values(updateObj), ...Object.values(whereObj)]] + ['UPDATE test_schema."test_table" SET content=$1, receipt_id=$2 WHERE account_id=$3 AND block_height=$4 RETURNING *', [...Object.values(updateObj), ...Object.values(whereObj)]] ]); }); @@ -129,7 +129,7 @@ describe('DML Handler tests', () => { await dmlHandler.upsert(SCHEMA, TABLE_NAME, inputObj, conflictCol, updateCol); expect(query.mock.calls).toEqual([ - ['INSERT INTO test_schema.test_table (account_id, block_height, receipt_id) VALUES (\'morgs_near\', \'1\', \'abc\'), (\'morgs_near\', \'2\', \'abc\') ON CONFLICT (account_id, block_height) DO UPDATE SET receipt_id = excluded.receipt_id RETURNING *', []] + ['INSERT INTO test_schema."test_table" (account_id, block_height, receipt_id) VALUES (\'morgs_near\', \'1\', \'abc\'), (\'morgs_near\', \'2\', \'abc\') ON CONFLICT (account_id, block_height) DO UPDATE SET receipt_id = excluded.receipt_id RETURNING *', []] ]); }); @@ -143,7 +143,7 @@ describe('DML Handler tests', () => { await dmlHandler.delete(SCHEMA, TABLE_NAME, inputObj); expect(query.mock.calls).toEqual([ - ['DELETE FROM test_schema.test_table WHERE account_id=$1 AND block_height=$2 RETURNING *', Object.values(inputObj)] + ['DELETE FROM test_schema."test_table" WHERE account_id=$1 AND block_height=$2 RETURNING *', Object.values(inputObj)] ]); }); }); diff --git a/runner/src/dml-handler/dml-handler.ts b/runner/src/dml-handler/dml-handler.ts index f060bfe14..15873e14f 100644 --- a/runner/src/dml-handler/dml-handler.ts +++ b/runner/src/dml-handler/dml-handler.ts @@ -3,6 +3,8 @@ import PgClientModule from '../pg-client'; import HasuraClient from '../hasura-client/hasura-client'; export default class DmlHandler { + validTableNameRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/; + private constructor ( private readonly pgClient: PgClientModule, ) {} @@ -32,9 +34,9 @@ export default class DmlHandler { const keys = Object.keys(objects[0]); // Get array of values from each object, and return array of arrays as result. Expects all objects to have the same number of items in same order const values = objects.map(obj => keys.map(key => obj[key])); - const query = `INSERT INTO ${schemaName}.${tableName} (${keys.join(', ')}) VALUES %L RETURNING *`; + const query = `INSERT INTO ${schemaName}."${tableName}" (${keys.join(', ')}) VALUES %L RETURNING *`; - const result = await wrapError(async () => await this.pgClient.query(this.pgClient.format(query, values), []), `Failed to execute '${query}' on ${schemaName}.${tableName}.`); + const result = await wrapError(async () => await this.pgClient.query(this.pgClient.format(query, values), []), `Failed to execute '${query}' on ${schemaName}."${tableName}".`); if (result.rows?.length === 0) { console.log('No rows were inserted.'); } @@ -45,12 +47,12 @@ export default class DmlHandler { const keys = Object.keys(object); const values = Object.values(object); const param = Array.from({ length: keys.length }, (_, index) => `${keys[index]}=$${index + 1}`).join(' AND '); - let query = `SELECT * FROM ${schemaName}.${tableName} WHERE ${param}`; + let query = `SELECT * FROM ${schemaName}."${tableName}" WHERE ${param}`; if (limit !== null) { query = query.concat(' LIMIT ', Math.round(limit).toString()); } - const result = await wrapError(async () => await this.pgClient.query(this.pgClient.format(query), values), `Failed to execute '${query}' on ${schemaName}.${tableName}.`); + const result = await wrapError(async () => await this.pgClient.query(this.pgClient.format(query), values), `Failed to execute '${query}' on ${schemaName}."${tableName}".`); if (!(result.rows && result.rows.length > 0)) { console.log('No rows were selected.'); } @@ -64,9 +66,9 @@ export default class DmlHandler { const whereParam = Array.from({ length: whereKeys.length }, (_, index) => `${whereKeys[index]}=$${index + 1 + updateKeys.length}`).join(' AND '); const queryValues = [...Object.values(updateObject), ...Object.values(whereObject)]; - const query = `UPDATE ${schemaName}.${tableName} SET ${updateParam} WHERE ${whereParam} RETURNING *`; + const query = `UPDATE ${schemaName}."${tableName}" SET ${updateParam} WHERE ${whereParam} RETURNING *`; - const result = await wrapError(async () => await this.pgClient.query(this.pgClient.format(query), queryValues), `Failed to execute '${query}' on ${schemaName}.${tableName}.`); + const result = await wrapError(async () => await this.pgClient.query(this.pgClient.format(query), queryValues), `Failed to execute '${query}' on ${schemaName}."${tableName}".`); if (!(result.rows && result.rows.length > 0)) { console.log('No rows were selected.'); } @@ -82,9 +84,9 @@ export default class DmlHandler { // Get array of values from each object, and return array of arrays as result. Expects all objects to have the same number of items in same order const values = objects.map(obj => keys.map(key => obj[key])); const updatePlaceholders = updateColumns.map(col => `${col} = excluded.${col}`).join(', '); - const query = `INSERT INTO ${schemaName}.${tableName} (${keys.join(', ')}) VALUES %L ON CONFLICT (${conflictColumns.join(', ')}) DO UPDATE SET ${updatePlaceholders} RETURNING *`; + const query = `INSERT INTO ${schemaName}."${tableName}" (${keys.join(', ')}) VALUES %L ON CONFLICT (${conflictColumns.join(', ')}) DO UPDATE SET ${updatePlaceholders} RETURNING *`; - const result = await wrapError(async () => await this.pgClient.query(this.pgClient.format(query, values), []), `Failed to execute '${query}' on ${schemaName}.${tableName}.`); + const result = await wrapError(async () => await this.pgClient.query(this.pgClient.format(query, values), []), `Failed to execute '${query}' on ${schemaName}."${tableName}".`); if (result.rows?.length === 0) { console.log('No rows were inserted or updated.'); } @@ -95,9 +97,9 @@ export default class DmlHandler { const keys = Object.keys(object); const values = Object.values(object); const param = Array.from({ length: keys.length }, (_, index) => `${keys[index]}=$${index + 1}`).join(' AND '); - const query = `DELETE FROM ${schemaName}.${tableName} WHERE ${param} RETURNING *`; + const query = `DELETE FROM ${schemaName}."${tableName}" WHERE ${param} RETURNING *`; - const result = await wrapError(async () => await this.pgClient.query(this.pgClient.format(query), values), `Failed to execute '${query}' on ${schemaName}.${tableName}.`); + const result = await wrapError(async () => await this.pgClient.query(this.pgClient.format(query), values), `Failed to execute '${query}' on ${schemaName}."${tableName}".`); if (!(result.rows && result.rows.length > 0)) { console.log('No rows were deleted.'); } diff --git a/runner/src/indexer/indexer.test.ts b/runner/src/indexer/indexer.test.ts index 4ba762130..37a828639 100644 --- a/runner/src/indexer/indexer.test.ts +++ b/runner/src/indexer/indexer.test.ts @@ -142,7 +142,7 @@ ADD CONSTRAINT "post_likes_post_id_fkey" FOREIGN KEY ("post_id") REFERENCES "posts" ("id") ON DELETE CASCADE ON UPDATE NO ACTION; CREATE TABLE IF NOT EXISTS - "public"."My Table1" (id serial PRIMARY KEY); + "My Table1" (id serial PRIMARY KEY); CREATE TABLE "Another-Table" (id serial PRIMARY KEY); @@ -462,6 +462,77 @@ CREATE TABLE ]); }); + test('GetTables works for a variety of input schemas', async () => { + const indexer = new Indexer('mainnet'); + + const simpleSchemaTables = indexer.getTableNames(SIMPLE_SCHEMA); + expect(simpleSchemaTables).toStrictEqual(['posts']); + + const socialSchemaTables = indexer.getTableNames(SOCIAL_SCHEMA); + expect(socialSchemaTables).toStrictEqual(['posts', 'comments', 'post_likes']); + + const stressTestSchemaTables = indexer.getTableNames(STRESS_TEST_SCHEMA); + expect(stressTestSchemaTables).toStrictEqual([ + 'creator_quest', + 'composer_quest', + 'contractor - quest', + 'posts', + 'comments', + 'post_likes', + 'My Table1', + 'Another-Table', + 'Third-Table', + 'yet_another_table']); + + // Test that duplicate table names throw an error + const duplicateTableSchema = `CREATE TABLE + "posts" ( + "id" SERIAL NOT NULL + ); + CREATE TABLE posts ( + "id" SERIAL NOT NULL + );`; + expect(() => { + indexer.getTableNames(duplicateTableSchema); + }).toThrow('Table posts already exists in schema. Table names must be unique. Quotes are not allowed as a differentiator between table names.'); + + // Test that schema with no tables throws an error + expect(() => { + indexer.getTableNames(''); + }).toThrow('Schema does not have any tables. There should be at least one table.'); + }); + + test('SanitizeTableName works properly on many test cases', async () => { + const indexer = new Indexer('mainnet'); + + expect(indexer.sanitizeTableName('table_name')).toStrictEqual('TableName'); + expect(indexer.sanitizeTableName('tablename')).toStrictEqual('Tablename'); // name is not capitalized + expect(indexer.sanitizeTableName('table name')).toStrictEqual('TableName'); + expect(indexer.sanitizeTableName('table!name!')).toStrictEqual('TableName'); + expect(indexer.sanitizeTableName('123TABle')).toStrictEqual('_123TABle'); // underscore at beginning + expect(indexer.sanitizeTableName('123_tABLE')).toStrictEqual('_123TABLE'); // underscore at beginning, capitalization + expect(indexer.sanitizeTableName('some-table_name')).toStrictEqual('SomeTableName'); + expect(indexer.sanitizeTableName('!@#$%^&*()table@)*&(%#')).toStrictEqual('Table'); // All special characters removed + expect(indexer.sanitizeTableName('T_name')).toStrictEqual('TName'); + expect(indexer.sanitizeTableName('_table')).toStrictEqual('Table'); // Starting underscore was removed + }); + + test('indexer fails to build context.db due to collision on sanitized table names', async () => { + const indexer = new Indexer('mainnet'); + + const schemaWithDuplicateSanitizedTableNames = `CREATE TABLE + "test table" ( + "id" SERIAL NOT NULL + ); + CREATE TABLE "test!table" ( + "id" SERIAL NOT NULL + );`; + + // Does not outright throw an error but instead returns an empty object + expect(indexer.buildDatabaseContext('test_account', 'test_schema_name', schemaWithDuplicateSanitizedTableNames, 1)) + .toStrictEqual({}); + }); + test('indexer builds context and inserts an objects into existing table', async () => { const mockDmlHandler: any = { create: jest.fn().mockImplementation(() => { @@ -489,7 +560,7 @@ CREATE TABLE accounts_liked: JSON.stringify(['cwpuzzles.near']) }]; - const result = await context.db.insert_posts(objToInsert); + const result = await context.db.Posts.insert(objToInsert); expect(result.length).toEqual(2); }); @@ -512,9 +583,9 @@ CREATE TABLE account_id: 'morgs_near', receipt_id: 'abc', }; - const result = await context.db.select_posts(objToSelect); + const result = await context.db.Posts.select(objToSelect); expect(result.length).toEqual(2); - const resultLimit = await context.db.select_posts(objToSelect, 1); + const resultLimit = await context.db.Posts.select(objToSelect, 1); expect(resultLimit.length).toEqual(1); }); @@ -543,7 +614,7 @@ CREATE TABLE content: 'test_content', block_timestamp: 805, }; - const result = await context.db.update_posts(whereObj, updateObj); + const result = await context.db.Posts.update(whereObj, updateObj); expect(result.length).toEqual(2); }); @@ -583,9 +654,9 @@ CREATE TABLE accounts_liked: JSON.stringify(['cwpuzzles.near']) }]; - let result = await context.db.upsert_posts(objToInsert, ['account_id', 'block_height'], ['content', 'block_timestamp']); + let result = await context.db.Posts.upsert(objToInsert, ['account_id', 'block_height'], ['content', 'block_timestamp']); expect(result.length).toEqual(2); - result = await context.db.upsert_posts(objToInsert[0], ['account_id', 'block_height'], ['content', 'block_timestamp']); + result = await context.db.Posts.upsert(objToInsert[0], ['account_id', 'block_height'], ['content', 'block_timestamp']); expect(result.length).toEqual(1); }); @@ -603,7 +674,7 @@ CREATE TABLE account_id: 'morgs_near', receipt_id: 'abc', }; - const result = await context.db.delete_posts(deleteFilter); + const result = await context.db.Posts.delete(deleteFilter); expect(result.length).toEqual(2); }); @@ -616,16 +687,34 @@ CREATE TABLE const context = indexer.buildContext(STRESS_TEST_SCHEMA, 'morgs.near/social_feed1', 1, 'postgres'); expect(Object.keys(context.db)).toStrictEqual([ - 'insert_creator_quest', 'select_creator_quest', 'update_creator_quest', 'upsert_creator_quest', 'delete_creator_quest', - 'insert_composer_quest', 'select_composer_quest', 'update_composer_quest', 'upsert_composer_quest', 'delete_composer_quest', - 'insert_contractor___quest', 'select_contractor___quest', 'update_contractor___quest', 'upsert_contractor___quest', 'delete_contractor___quest', - 'insert_posts', 'select_posts', 'update_posts', 'upsert_posts', 'delete_posts', - 'insert_comments', 'select_comments', 'update_comments', 'upsert_comments', 'delete_comments', - 'insert_post_likes', 'select_post_likes', 'update_post_likes', 'upsert_post_likes', 'delete_post_likes', - 'insert_My_Table1', 'select_My_Table1', 'update_My_Table1', 'upsert_My_Table1', 'delete_My_Table1', - 'insert_Another_Table', 'select_Another_Table', 'update_Another_Table', 'upsert_Another_Table', 'delete_Another_Table', - 'insert_Third_Table', 'select_Third_Table', 'update_Third_Table', 'upsert_Third_Table', 'delete_Third_Table', - 'insert_yet_another_table', 'select_yet_another_table', 'update_yet_another_table', 'upsert_yet_another_table', 'delete_yet_another_table']); + 'CreatorQuest', + 'ComposerQuest', + 'ContractorQuest', + 'Posts', + 'Comments', + 'PostLikes', + 'MyTable1', + 'AnotherTable', + 'ThirdTable', + 'YetAnotherTable']); + expect(Object.keys(context.db.CreatorQuest)).toStrictEqual([ + 'insert', + 'select', + 'update', + 'upsert', + 'delete']); + expect(Object.keys(context.db.PostLikes)).toStrictEqual([ + 'insert', + 'select', + 'update', + 'upsert', + 'delete']); + expect(Object.keys(context.db.MyTable1)).toStrictEqual([ + 'insert', + 'select', + 'update', + 'upsert', + 'delete']); }); test('indexer builds context and returns empty array if failed to generate db methods', async () => { diff --git a/runner/src/indexer/indexer.ts b/runner/src/indexer/indexer.ts index a6bb9aebe..3a2858cd5 100644 --- a/runner/src/indexer/indexer.ts +++ b/runner/src/indexer/indexer.ts @@ -2,6 +2,7 @@ import fetch, { type Response } from 'node-fetch'; import { VM } from 'vm2'; import AWS from 'aws-sdk'; import { Block } from '@near-lake/primitives'; +import { Parser } from 'node-sql-parser'; import Provisioner from '../provisioner'; import DmlHandler from '../dml-handler/dml-handler'; @@ -11,6 +12,7 @@ interface Dependencies { s3: AWS.S3 provisioner: Provisioner DmlHandler: typeof DmlHandler + parser: Parser }; interface Context { @@ -18,7 +20,7 @@ interface Context { set: (key: string, value: any) => Promise log: (...log: any[]) => Promise fetchFromSocialApi: (path: string, options?: any) => Promise - db: Record any> + db: Record any>> } interface IndexerFunction { @@ -45,6 +47,7 @@ export default class Indexer { s3: new AWS.S3(), provisioner: new Provisioner(), DmlHandler, + parser: new Parser(), ...deps, }; } @@ -219,83 +222,127 @@ export default class Indexer { }; } - validateTableNames (tableNames: string[]): void { - if (!(Array.isArray(tableNames) && tableNames.length > 0)) { - throw new Error('Schema does not have any tables. There should be at least one table.'); - } - const correctTableNameFormat = /^[a-zA-Z_][a-zA-Z0-9_]*$/; + getTableNames (schema: string): string[] { + let schemaSyntaxTree = this.deps.parser.astify(schema, { database: 'Postgresql' }); + schemaSyntaxTree = Array.isArray(schemaSyntaxTree) ? schemaSyntaxTree : [schemaSyntaxTree]; // Ensure iterable + const tableNames = new Set(); - tableNames.forEach(name => { - if (!name.includes('"') && !correctTableNameFormat.test(name)) { // Only test if table name doesn't have quotes - throw new Error(`Table name ${name} is not formatted correctly. Table names must not start with a number and only contain alphanumerics or underscores.`); - } - }); - } + // Collect all table names from schema AST, throw error if duplicate table names exist + for (const statement of schemaSyntaxTree) { + if (statement.type === 'create' && statement.keyword === 'table' && statement.table !== undefined) { + const tableName: string = statement.table[0].table; - getTableNames (schema: string): string[] { - const tableRegex = /CREATE TABLE\s+(?:IF NOT EXISTS\s+)?"?(.+?)"?\s*\(/g; - const tableNames = Array.from(schema.matchAll(tableRegex), match => { - let tableName; - if (match[1].includes('.')) { // If expression after create has schemaName.tableName, return only tableName - tableName = match[1].split('.')[1]; - tableName = tableName.startsWith('"') ? tableName.substring(1) : tableName; - } else { - tableName = match[1]; + if (tableNames.has(tableName)) { + throw new Error(`Table ${tableName} already exists in schema. Table names must be unique. Quotes are not allowed as a differentiator between table names.`); + } + + tableNames.add(tableName); } - return /^\w+$/.test(tableName) ? tableName : `"${tableName}"`; // If table name has special characters, it must be inside double quotes - }); - this.validateTableNames(tableNames); - console.log('Retrieved the following table names from schema: ', tableNames); - return tableNames; + } + + // Ensure schema is not empty + if (tableNames.size === 0) { + throw new Error('Schema does not have any tables. There should be at least one table.'); + } + + const tableNamesArray = Array.from(tableNames); + console.log('Retrieved the following table names from schema: ', tableNamesArray); + return Array.from(tableNamesArray); } sanitizeTableName (tableName: string): string { - tableName = tableName.startsWith('"') && tableName.endsWith('"') ? tableName.substring(1, tableName.length - 1) : tableName; - return tableName.replace(/[^a-zA-Z0-9_]/g, '_'); + // Convert to PascalCase + let pascalCaseTableName = tableName + // Replace special characters with underscores + .replace(/[^a-zA-Z0-9_]/g, '_') + // Makes first letter and any letters following an underscore upper case + .replace(/^([a-zA-Z])|_([a-zA-Z])/g, (match: string) => match.toUpperCase()) + // Removes all underscores + .replace(/_/g, ''); + + // Add underscore if first character is a number + if (/^[0-9]/.test(pascalCaseTableName)) { + pascalCaseTableName = '_' + pascalCaseTableName; + } + + return pascalCaseTableName; } - buildDatabaseContext (account: string, schemaName: string, schema: string, blockHeight: number): Record any> { + buildDatabaseContext (account: string, schemaName: string, schema: string, blockHeight: number): Record any>> { try { const tables = this.getTableNames(schema); + const sanitizedTableNames = new Set(); let dmlHandler: DmlHandler; - // TODO: Refactor object to be context.db.[table_name].[insert, select, update, upsert, delete] + + // Generate and collect methods for each table name const result = tables.reduce((prev, tableName) => { + // Generate sanitized table name and ensure no conflict const sanitizedTableName = this.sanitizeTableName(tableName); + if (sanitizedTableNames.has(sanitizedTableName)) { + throw new Error(`Table ${tableName} has the same sanitized name as another table. Special characters are removed to generate context.db methods. Please rename the table.`); + } else { + sanitizedTableNames.add(sanitizedTableName); + } + + // Generate context.db methods for table + const defaultLog = `Calling context.db.${sanitizedTableName}.`; const funcForTable = { - [`insert_${sanitizedTableName}`]: async (objects: any) => { - await this.writeLog(`context.db.insert_${sanitizedTableName}`, blockHeight, - `Calling context.db.insert_${sanitizedTableName}.`, - `Inserting object ${JSON.stringify(objects)} into table ${tableName} on schema ${schemaName}`); - dmlHandler = dmlHandler ?? await this.deps.DmlHandler.create(account); - return await dmlHandler.insert(schemaName, tableName, Array.isArray(objects) ? objects : [objects]); - }, - [`select_${sanitizedTableName}`]: async (object: any, limit = null) => { - await this.writeLog(`context.db.select_${sanitizedTableName}`, blockHeight, - `Calling context.db.select_${sanitizedTableName}.`, - `Selecting objects with values ${JSON.stringify(object)} in table ${tableName} on schema ${schemaName} with ${limit === null ? 'no' : limit} limit`); - dmlHandler = dmlHandler ?? await this.deps.DmlHandler.create(account); - return await dmlHandler.select(schemaName, tableName, object, limit); - }, - [`update_${sanitizedTableName}`]: async (whereObj: any, updateObj: any) => { - await this.writeLog(`context.db.update_${sanitizedTableName}`, blockHeight, - `Calling context.db.update_${sanitizedTableName}.`, - `Updating objects that match ${JSON.stringify(whereObj)} with values ${JSON.stringify(updateObj)} in table ${tableName} on schema ${schemaName}`); - dmlHandler = dmlHandler ?? await this.deps.DmlHandler.create(account); - return await dmlHandler.update(schemaName, tableName, whereObj, updateObj); - }, - [`upsert_${sanitizedTableName}`]: async (objects: any, conflictColumns: string[], updateColumns: string[]) => { - await this.writeLog(`context.db.upsert_${sanitizedTableName}`, blockHeight, - `Calling context.db.upsert_${sanitizedTableName}.`, - `Inserting objects with values ${JSON.stringify(objects)} in table ${tableName} on schema ${schemaName}. Conflict on columns ${conflictColumns.join(', ')} will update values in columns ${updateColumns.join(', ')}`); - dmlHandler = dmlHandler ?? await this.deps.DmlHandler.create(account); - return await dmlHandler.upsert(schemaName, tableName, Array.isArray(objects) ? objects : [objects], conflictColumns, updateColumns); - }, - [`delete_${sanitizedTableName}`]: async (object: any) => { - await this.writeLog(`context.db.delete_${sanitizedTableName}`, blockHeight, - `Calling context.db.delete_${sanitizedTableName}.`, - `Deleting objects with values ${JSON.stringify(object)} in table ${tableName} on schema ${schemaName}`); - dmlHandler = dmlHandler ?? await this.deps.DmlHandler.create(account); - return await dmlHandler.delete(schemaName, tableName, object); + [`${sanitizedTableName}`]: { + insert: async (objectsToInsert: any) => { + // Write log before calling insert + await this.writeLog(`context.db.${sanitizedTableName}.insert`, blockHeight, defaultLog + '.insert', + `Inserting object ${JSON.stringify(objectsToInsert)} into table ${tableName} on schema ${schemaName}`); + + // Create DmlHandler if it doesn't exist + dmlHandler = dmlHandler ?? await this.deps.DmlHandler.create(account); + + // Call insert with parameters + return await dmlHandler.insert(schemaName, tableName, Array.isArray(objectsToInsert) ? objectsToInsert : [objectsToInsert]); + }, + select: async (filterObj: any, limit = null) => { + // Write log before calling select + await this.writeLog(`context.db.${sanitizedTableName}.select`, blockHeight, defaultLog + '.select', + `Selecting objects with values ${JSON.stringify(filterObj)} in table ${tableName} on schema ${schemaName} with ${limit === null ? 'no' : limit} limit`); + + // Create DmlHandler if it doesn't exist + dmlHandler = dmlHandler ?? await this.deps.DmlHandler.create(account); + + // Call select with parameters + return await dmlHandler.select(schemaName, tableName, filterObj, limit); + }, + update: async (filterObj: any, updateObj: any) => { + // Write log before calling update + await this.writeLog(`context.db.${sanitizedTableName}.update`, blockHeight, defaultLog + '.update', + `Updating objects that match ${JSON.stringify(filterObj)} with values ${JSON.stringify(updateObj)} in table ${tableName} on schema ${schemaName}`); + + // Create DmlHandler if it doesn't exist + dmlHandler = dmlHandler ?? await this.deps.DmlHandler.create(account); + + // Call update with parameters + return await dmlHandler.update(schemaName, tableName, filterObj, updateObj); + }, + upsert: async (objectsToInsert: any, conflictColumns: string[], updateColumns: string[]) => { + // Write log before calling upsert + await this.writeLog(`context.db.${sanitizedTableName}.upsert`, blockHeight, defaultLog + '.upsert', + `Inserting objects with values ${JSON.stringify(objectsToInsert)} into table ${tableName} on schema ${schemaName}. Conflict on columns ${conflictColumns.join(', ')} will update values in columns ${updateColumns.join(', ')}`); + + // Create DmlHandler if it doesn't exist + dmlHandler = dmlHandler ?? await this.deps.DmlHandler.create(account); + + // Call upsert with parameters + return await dmlHandler.upsert(schemaName, tableName, Array.isArray(objectsToInsert) ? objectsToInsert : [objectsToInsert], conflictColumns, updateColumns); + }, + delete: async (filterObj: any) => { + // Write log before calling delete + await this.writeLog(`context.db.${sanitizedTableName}.delete`, blockHeight, defaultLog + '.delete', + `Deleting objects with values ${JSON.stringify(filterObj)} from table ${tableName} on schema ${schemaName}`); + + // Create DmlHandler if it doesn't exist + dmlHandler = dmlHandler ?? await this.deps.DmlHandler.create(account); + + // Call delete with parameters + return await dmlHandler.delete(schemaName, tableName, filterObj); + } } }; From b80842f31d817d5dc2f187c0e515b34acd3bd0d8 Mon Sep 17 00:00:00 2001 From: Morgan McCauley Date: Thu, 28 Sep 2023 08:58:38 +1300 Subject: [PATCH 29/31] feat: Add `indexer_log_entries` indexes to `hasura` migrations (#238) --- hasura/migrations/default/1691364619300_init/up.sql | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/hasura/migrations/default/1691364619300_init/up.sql b/hasura/migrations/default/1691364619300_init/up.sql index 9b7f5dd94..4c1dad71f 100644 --- a/hasura/migrations/default/1691364619300_init/up.sql +++ b/hasura/migrations/default/1691364619300_init/up.sql @@ -1,4 +1,5 @@ SET check_function_bodies = false; + CREATE TABLE public.indexer_log_entries ( id uuid DEFAULT gen_random_uuid() NOT NULL, function_name text NOT NULL, @@ -6,13 +7,19 @@ CREATE TABLE public.indexer_log_entries ( "timestamp" timestamp without time zone DEFAULT CURRENT_TIMESTAMP, message text ); + CREATE TABLE public.indexer_state ( function_name character varying NOT NULL, current_block_height numeric(21,0) NOT NULL, status text, current_historical_block_height numeric(21,0) ); + ALTER TABLE ONLY public.indexer_log_entries ADD CONSTRAINT indexer_log_entries_pkey PRIMARY KEY (id); + ALTER TABLE ONLY public.indexer_state ADD CONSTRAINT indexer_state_pkey PRIMARY KEY (function_name); + +CREATE INDEX idx_function_name ON indexer_log_entries(function_name); +CREATE INDEX idx_timestamp ON indexer_log_entries("timestamp"); From bf0cbf5a7ca0097e95100a25ef8cdaa66d0c5596 Mon Sep 17 00:00:00 2001 From: Isha <131987812+Ishatt@users.noreply.github.com> Date: Thu, 28 Sep 2023 11:34:20 -0400 Subject: [PATCH 30/31] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 38 ++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/epic-template-.md | 30 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/epic-template-.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..dd84ea782 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/epic-template-.md b/.github/ISSUE_TEMPLATE/epic-template-.md new file mode 100644 index 000000000..e79b09fa7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/epic-template-.md @@ -0,0 +1,30 @@ +--- +name: 'Epic Template ' +about: 'Epics are milestones or groups of alike issues ' +title: "\U0001F537 [Epic] New Epic " +labels: '' +assignees: '' + +--- + +### Description +(Overview of milestone or function governed by this epic) +### Success Criteria +(Evaluate how this epic could be considered as complete and success) +### Resources +(Relevant documentation, Figma links, and other reference material) +Item 1 +Item 2 +Item 3 +```[tasklist] +### Child Issues +[ ] https://github.com/near/github-project-test/issues/1 +[ ] https://github.com/near/github-project-test/issues/2 +[ ] https://github.com/near/github-project-test/issues/3 +``` +```[tasklist] +### dependencies/blocked +[ ] https://github.com/near/github-project-test/issues/1 +[ ] https://github.com/near/github-project-test/issues/2 +[ ] https://github.com/near/github-project-test/issues/3 +``` From 7f6d3b65ed7a2d807de7514f64836f73a89829a4 Mon Sep 17 00:00:00 2001 From: Isha <131987812+Ishatt@users.noreply.github.com> Date: Thu, 28 Sep 2023 11:36:58 -0400 Subject: [PATCH 31/31] Update issue templates --- .github/ISSUE_TEMPLATE/feature-request-.md | 20 ++++++++++++++++ .../ISSUE_TEMPLATE/secondary-focus-area-.md | 23 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/feature-request-.md create mode 100644 .github/ISSUE_TEMPLATE/secondary-focus-area-.md diff --git a/.github/ISSUE_TEMPLATE/feature-request-.md b/.github/ISSUE_TEMPLATE/feature-request-.md new file mode 100644 index 000000000..17d38632d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request-.md @@ -0,0 +1,20 @@ +--- +name: 'Feature Request ' +about: Suggest an idea for this project. If this doesn't look right +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/secondary-focus-area-.md b/.github/ISSUE_TEMPLATE/secondary-focus-area-.md new file mode 100644 index 000000000..f503606ed --- /dev/null +++ b/.github/ISSUE_TEMPLATE/secondary-focus-area-.md @@ -0,0 +1,23 @@ +--- +name: 'Secondary Focus Area ' +about: This issue serves to help us propose and organize support for impactful work, + as a secondary priority to epics & planned roadmap items. If this doesn't look right +title: "\U0001F525 [Secondary Focus Area] " +labels: '' +assignees: '' + +--- + +**Motivation** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. + +**Open questions**