From 59895f937ecdca4ea76f248fb6653573f41a9ca0 Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Thu, 4 Dec 2025 14:55:23 -0500 Subject: [PATCH 01/20] Add redis docker config --- docker/docker-compose.indexable.yml | 3 +++ docker/docker-compose.yml | 8 ++++++++ docker/start.sh | 9 ++++++--- package.json | 4 ++-- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docker/docker-compose.indexable.yml b/docker/docker-compose.indexable.yml index 73abed2..37661a3 100644 --- a/docker/docker-compose.indexable.yml +++ b/docker/docker-compose.indexable.yml @@ -5,6 +5,9 @@ services: postgres: networks: - api_indexable_network + redis: + networks: + - api_indexable_network networks: api_indexable_network: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index fef1ce4..9a629b7 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -25,3 +25,11 @@ services: POSTGRES_PASSWORD: beanstalk volumes: - ./.data/${DOCKER_ENV}/postgres:/var/lib/postgresql/data + redis: + image: redis:8-alpine + ports: + - "${REDIS_PORT}:6379" + command: ["redis-server", "--appendonly", "yes"] + volumes: + - ./.data/${DOCKER_ENV}/redis:/data + restart: unless-stopped \ No newline at end of file diff --git a/docker/start.sh b/docker/start.sh index 595f195..e22a4d8 100755 --- a/docker/start.sh +++ b/docker/start.sh @@ -5,20 +5,23 @@ cd $(dirname "$0") DOCKER_ENV=$1 KOAJS_PORT=$2 POSTGRES_PORT=$3 -SERVICE=$4 -if [ -z "$DOCKER_ENV" ] || [ -z "$KOAJS_PORT" ] || [ -z "$POSTGRES_PORT" ]; then +REDIS_PORT=$4 +SERVICES=("${@:5}") +if [ -z "$DOCKER_ENV" ] || [ -z "$KOAJS_PORT" ] || [ -z "$POSTGRES_PORT" ] || [ -z "$REDIS_PORT" ]; then DOCKER_ENV="dev" KOAJS_PORT="4000" POSTGRES_PORT="6432" + REDIS_PORT="7379" fi export DOCKER_ENV export KOAJS_PORT export POSTGRES_PORT +export REDIS_PORT # Can optionally provide a specific service to start. Defaults to all docker compose \ -p web-api-$DOCKER_ENV \ -f docker-compose.yml \ ${DOCKER_ENV:+$([[ "$DOCKER_ENV" == "local" ]] && echo "-f docker-compose.indexable.yml")} \ - up -d ${SERVICE:+$SERVICE} + up -d "${SERVICES[@]}" diff --git a/package.json b/package.json index 7e096d5..51b0194 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,9 @@ "main": "src/app.js", "scripts": { "start": "node ./src/app.js", - "docker": "./docker/build.sh local && ./docker/start.sh local 3000 5432", + "docker": "./docker/build.sh local && ./docker/start.sh local 3000 5432 6379", "docker:stop": "./docker/stop.sh local", - "docker:db": "./docker/start.sh local 3000 5432 postgres", + "docker:db": "./docker/start.sh local 3000 5432 6379 postgres redis", "test": "jest", "debug": "node --inspect ./src/app.js", "prettier": "prettier --write .", From 53e2f6a6b5df6e1b9bdabf0d2555024aaf1fe96a Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Thu, 4 Dec 2025 15:07:38 -0500 Subject: [PATCH 02/20] Redis working --- package-lock.json | 109 +++++++++++++++++++++++++++++----- package.json | 1 + src/app.js | 3 + src/routes/sg-cache-routes.js | 70 ++++++++++++++++++++++ 4 files changed, 168 insertions(+), 15 deletions(-) create mode 100644 src/routes/sg-cache-routes.js diff --git a/package-lock.json b/package-lock.json index e8c7e54..744a9c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "node-cron": "^3.0.3", "pg": "^8.12.0", "pg-hstore": "^2.3.4", + "redis": "^5.10.0", "sequelize": "^6.37.4", "sequelize-cli": "^6.6.2" }, @@ -1671,6 +1672,61 @@ "node": ">=14" } }, + "node_modules/@redis/bloom": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.10.0.tgz", + "integrity": "sha512-doIF37ob+l47n0rkpRNgU8n4iacBlKM9xLiP1LtTZTvz8TloJB8qx/MgvhMhKdYG+CvCY2aPBnN2706izFn/4A==", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.10.0" + } + }, + "node_modules/@redis/client": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.10.0.tgz", + "integrity": "sha512-JXmM4XCoso6C75Mr3lhKA3eNxSzkYi3nCzxDIKY+YOszYsJjuKbFgVtguVPbLMOttN4iu2fXoc2BGhdnYhIOxA==", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@redis/json": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.10.0.tgz", + "integrity": "sha512-B2G8XlOmTPUuZtD44EMGbtoepQG34RCDXLZbjrtON1Djet0t5Ri7/YPXvL9aomXqP8lLTreaprtyLKF4tmXEEA==", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.10.0" + } + }, + "node_modules/@redis/search": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.10.0.tgz", + "integrity": "sha512-3SVcPswoSfp2HnmWbAGUzlbUPn7fOohVu2weUQ0S+EMiQi8jwjL+aN2p6V3TI65eNfVsJ8vyPvqWklm6H6esmg==", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.10.0" + } + }, + "node_modules/@redis/time-series": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.10.0.tgz", + "integrity": "sha512-cPkpddXH5kc/SdRhF0YG0qtjL+noqFT0AcHbQ6axhsPsO7iqPi1cjxgdkE9TNeKiBUUdCaU1DbqkR/LzbzPBhg==", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.10.0" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1942,9 +1998,9 @@ } }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -2335,6 +2391,14 @@ "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", @@ -4288,9 +4352,9 @@ } }, "node_modules/js-beautify/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -4340,9 +4404,9 @@ "dev": true }, "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==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "dependencies": { "argparse": "^1.0.7", @@ -4414,9 +4478,9 @@ } }, "node_modules/koa": { - "version": "2.16.2", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.2.tgz", - "integrity": "sha512-+CCssgnrWKx9aI3OeZwroa/ckG4JICxvIFnSiOUyl2Uv+UTI+xIw0FfFrWS7cQFpoePpr9o8csss7KzsTzNL8Q==", + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.3.tgz", + "integrity": "sha512-zPPuIt+ku1iCpFBRwseMcPYQ1cJL8l60rSmKeOuGfOXyE6YnTBmf2aEFNL2HQGrD0cPcLO/t+v9RTgC+fwEh/g==", "dependencies": { "accepts": "^1.3.5", "cache-content-type": "^1.0.0", @@ -5340,6 +5404,21 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, + "node_modules/redis": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-5.10.0.tgz", + "integrity": "sha512-0/Y+7IEiTgVGPrLFKy8oAEArSyEJkU0zvgV5xyi9NzNQ+SLZmyFbUsWIbgPcd4UdUh00opXGKlXJwMmsis5Byw==", + "dependencies": { + "@redis/bloom": "5.10.0", + "@redis/client": "5.10.0", + "@redis/json": "5.10.0", + "@redis/search": "5.10.0", + "@redis/time-series": "5.10.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6043,9 +6122,9 @@ } }, "node_modules/validator": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", - "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "version": "13.15.23", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.23.tgz", + "integrity": "sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==", "engines": { "node": ">= 0.10" } diff --git a/package.json b/package.json index 51b0194..99a6291 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "node-cron": "^3.0.3", "pg": "^8.12.0", "pg-hstore": "^2.3.4", + "redis": "^5.10.0", "sequelize": "^6.37.4", "sequelize-cli": "^6.6.2" }, diff --git a/src/app.js b/src/app.js index b492ae2..0c70edd 100644 --- a/src/app.js +++ b/src/app.js @@ -7,6 +7,7 @@ const fieldRoutes = require('./routes/field-routes.js'); const proxyRoutes = require('./routes/proxy-routes.js'); const seasonRoutes = require('./routes/season-routes.js'); const inflowRoutes = require('./routes/inflow-routes.js'); +const sgCacheRoutes = require('./routes/sg-cache-routes.js'); const Koa = require('koa'); const bodyParser = require('koa-bodyparser'); @@ -117,6 +118,8 @@ async function appStartup() { app.use(seasonRoutes.allowedMethods()); app.use(inflowRoutes.routes()); app.use(inflowRoutes.allowedMethods()); + app.use(sgCacheRoutes.routes()); + app.use(sgCacheRoutes.allowedMethods()); const router = new Router(); router.get('/healthcheck', async (ctx) => { diff --git a/src/routes/sg-cache-routes.js b/src/routes/sg-cache-routes.js new file mode 100644 index 0000000..ccd6f47 --- /dev/null +++ b/src/routes/sg-cache-routes.js @@ -0,0 +1,70 @@ +const Router = require('koa-router'); +const { createClient } = require('redis'); + +const router = new Router({ + prefix: '/sg-cache' +}); + +/* Temporary implementation for testing redis integration */ + +const redis = createClient({ + url: 'redis://localhost:6379' +}); +redis.connect(); + +/** + * Reads a value from the cache by key + * ?key: the cache key to read + */ +router.get('/', async (ctx) => { + const key = ctx.query.key; + + if (!key) { + ctx.status = 400; + ctx.body = { + message: 'Query parameter `key` is required.' + }; + return; + } + + const value = JSON.parse(await redis.get(key)); + + ctx.body = { + key, + value + }; +}); + +/** + * Writes a value to the cache + * Body should contain: { key: string, value: any } + */ +router.post('/', async (ctx) => { + const { key, value } = ctx.request.body; + + if (!key) { + ctx.status = 400; + ctx.body = { + message: 'Body parameter `key` is required.' + }; + return; + } + + if (value === undefined) { + ctx.status = 400; + ctx.body = { + message: 'Body parameter `value` is required.' + }; + return; + } + + await redis.set(key, JSON.stringify(value)); + + ctx.body = { + success: true, + key, + message: 'Value cached successfully' // Placeholder - will be replaced with actual cache operation + }; +}); + +module.exports = router; From 34ead6d57f56503274ac6c19aa08c8b358f71629 Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:43:55 -0500 Subject: [PATCH 03/20] Identifies all fields per entity --- src/routes/sg-cache-routes.js | 200 +++++++++++++++++++++++++++++++++- 1 file changed, 199 insertions(+), 1 deletion(-) diff --git a/src/routes/sg-cache-routes.js b/src/routes/sg-cache-routes.js index ccd6f47..662c5eb 100644 --- a/src/routes/sg-cache-routes.js +++ b/src/routes/sg-cache-routes.js @@ -1,11 +1,22 @@ const Router = require('koa-router'); const { createClient } = require('redis'); +const SubgraphQueryUtil = require('../utils/subgraph-query'); +const { C } = require('../constants/runtime-constants'); +const { gql } = require('graphql-request'); const router = new Router({ prefix: '/sg-cache' }); -/* Temporary implementation for testing redis integration */ +/* Temporary area for testing redis integration */ +// Configuration for which entities should be cached +// Need some control for which results go into the permanent storage, or when results move to permanent storage +// - better to write to it each hour, or only after a certain amount builds up? +// Need secondary kv store to track what is the current latest season captured within the permanent storage (for each entity) +// I suppose entities could track on something other than season? This should also be configurable. +// Need to clear cache upon new sg version; another reason its good to put this all in sg proxy? or can inspect header. + +// -> next step: endpoint for retrieving a specific entity; underlying behavior should be to retrieve from cache and from sg const redis = createClient({ url: 'redis://localhost:6379' @@ -27,6 +38,9 @@ router.get('/', async (ctx) => { return; } + await testIntrospect(); + await testSnap(); + const value = JSON.parse(await redis.get(key)); ctx.body = { @@ -68,3 +82,187 @@ router.post('/', async (ctx) => { }); module.exports = router; + +// Must be List queries that dont require explicitly provided id (in subgraph framework, usually ending in 's') +const config = { + beanstalk: { + queries: [ + { + name: 'cached_siloHourlySnapshots', + underlying: { + name: 'siloHourlySnapshots', + where: 'silo: "0xd1a0d188e861ed9d15773a2f3574a2e94134ba8f"', + paginationSettings: { + field: 'season', + lastValue: 0, + direction: 'asc' + } + } + }, + { + name: 'cached_fieldHourlySnapshots', + underlying: { + name: 'fieldHourlySnapshots', + where: 'field: "0xd1a0d188e861ed9d15773a2f3574a2e94134ba8f"', + paginationSettings: { + field: 'season', + lastValue: 0, + direction: 'asc' + } + } + } + ] + } +}; + +const testIntrospect = async (sgName, c = C()) => { + const introspection = await c.SG[sgName.toUpperCase()](gql` + query IntrospectionQuery { + __schema { + queryType { + name + } + mutationType { + name + } + subscriptionType { + name + } + types { + ...FullType + } + directives { + name + description + locations + args { + ...InputValue + } + } + } + } + fragment FullType on __Type { + kind + name + description + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } + } + fragment InputValue on __InputValue { + name + description + type { + ...TypeRef + } + defaultValue + } + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } + } + `); + + // Find the underlying types for each enabled query + const queryInfo = {}; + const queryTypes = introspection.__schema.types.find((t) => t.kind === 'OBJECT' && t.name === 'Query'); + for (const field of queryTypes.fields) { + const configQuery = config[sgName].queries.find((q) => q.underlying.name === field.name); + if (configQuery) { + let type = field.type; + while (type.ofType) { + type = type.ofType; + } + queryInfo[configQuery.name] = { + type: type.name + }; + } + } + + // Identify all fields accessible for each query + for (const query in queryInfo) { + const queryObject = introspection.__schema.types.find( + (t) => t.kind === 'OBJECT' && t.name === queryInfo[query].type + ); + queryInfo[query].fields = queryObject.fields.map((f) => f.name); + } + + console.log(queryInfo); + console.log(Object.keys(queryInfo).map((k) => `{ ${k} { ${queryInfo[k].fields.join(' ')} } }`)); + + await redis.set('introspection:beanstalk', JSON.stringify(queryInfo)); +}; + +const testSnap = async (c = C()) => { + const siloHourlySnapshots = await SubgraphQueryUtil.allPaginatedSG( + c.SG.BEANSTALK, + ` + { siloHourlySnapshots { id season silo stalk depositedBDV plantedBeans roots germinatingStalk penalizedStalkConvertDown unpenalizedStalkConvertDown avgConvertDownPenalty bonusStalkConvertUp totalBdvConvertUpBonus totalBdvConvertUp beanMints plantableStalk beanToMaxLpGpPerBdvRatio cropRatio avgGrownStalkPerBdvPerSeason grownStalkPerSeason convertDownPenalty activeFarmers deltaStalk deltaDepositedBDV deltaPlantedBeans deltaRoots deltaGerminatingStalk deltaPenalizedStalkConvertDown deltaUnpenalizedStalkConvertDown deltaAvgConvertDownPenalty deltaBonusStalkConvertUp deltaTotalBdvConvertUpBonus deltaTotalBdvConvertUp deltaBeanMints deltaPlantableStalk deltaBeanToMaxLpGpPerBdvRatio deltaCropRatio deltaAvgGrownStalkPerBdvPerSeason deltaGrownStalkPerSeason deltaConvertDownPenalty deltaActiveFarmers createdAt updatedAt caseId } } + `, + '', + `silo: "${c.BEANSTALK}"`, + { + field: 'season', + lastValue: 0, + direction: 'asc' + } + ); + console.log(siloHourlySnapshots.length); + console.log(siloHourlySnapshots.map((s) => s.season)); +}; + +if (require.main === module) { + (async () => { + await testIntrospect('beanstalk'); + // await testSnap(); + // console.log(JSON.parse(await redis.get('introspection:beanstalk'))); + })(); +} From 974b8fe455c3df271ee009c7ee174bb6df146873 Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:06:07 -0500 Subject: [PATCH 04/20] uses cached introspection when available --- src/routes/sg-cache-routes.js | 248 +++++++++------------------------- 1 file changed, 62 insertions(+), 186 deletions(-) diff --git a/src/routes/sg-cache-routes.js b/src/routes/sg-cache-routes.js index 662c5eb..7c75ce4 100644 --- a/src/routes/sg-cache-routes.js +++ b/src/routes/sg-cache-routes.js @@ -2,7 +2,7 @@ const Router = require('koa-router'); const { createClient } = require('redis'); const SubgraphQueryUtil = require('../utils/subgraph-query'); const { C } = require('../constants/runtime-constants'); -const { gql } = require('graphql-request'); +const axios = require('axios'); const router = new Router({ prefix: '/sg-cache' @@ -28,18 +28,14 @@ redis.connect(); * ?key: the cache key to read */ router.get('/', async (ctx) => { - const key = ctx.query.key; + const queryName = ctx.query.queryName; - if (!key) { - ctx.status = 400; - ctx.body = { - message: 'Query parameter `key` is required.' - }; - return; - } + // Derive the latest season number already retrieved per query. And then retreive all gte that one. + // Latest season will get rewritten always since it can be updating, and any further seasons would get appended to the cache. + // Then can respond to user request for specific fields - await testIntrospect(); - await testSnap(); + const queryInfo = await testIntrospect(); + const t = await testSnap(queryName, queryInfo); const value = JSON.parse(await redis.get(key)); @@ -49,179 +45,62 @@ router.get('/', async (ctx) => { }; }); -/** - * Writes a value to the cache - * Body should contain: { key: string, value: any } - */ -router.post('/', async (ctx) => { - const { key, value } = ctx.request.body; - - if (!key) { - ctx.status = 400; - ctx.body = { - message: 'Body parameter `key` is required.' - }; - return; - } - - if (value === undefined) { - ctx.status = 400; - ctx.body = { - message: 'Body parameter `value` is required.' - }; - return; - } - - await redis.set(key, JSON.stringify(value)); - - ctx.body = { - success: true, - key, - message: 'Value cached successfully' // Placeholder - will be replaced with actual cache operation - }; -}); - module.exports = router; // Must be List queries that dont require explicitly provided id (in subgraph framework, usually ending in 's') const config = { - beanstalk: { - queries: [ - { - name: 'cached_siloHourlySnapshots', - underlying: { - name: 'siloHourlySnapshots', - where: 'silo: "0xd1a0d188e861ed9d15773a2f3574a2e94134ba8f"', - paginationSettings: { - field: 'season', - lastValue: 0, - direction: 'asc' - } - } - }, - { - name: 'cached_fieldHourlySnapshots', - underlying: { - name: 'fieldHourlySnapshots', - where: 'field: "0xd1a0d188e861ed9d15773a2f3574a2e94134ba8f"', - paginationSettings: { - field: 'season', - lastValue: 0, - direction: 'asc' - } - } - } - ] + cached_siloHourlySnapshots: { + subgraph: 'pintostalk', + queryName: 'siloHourlySnapshots', + client: (c) => c.SG.BEANSTALK, + // TODO: ideally we could cache by farmers too? not sure if the ui actually uses this currently + where: 'silo: "0xd1a0d188e861ed9d15773a2f3574a2e94134ba8f"', + paginationSettings: { + field: 'season', + lastValue: 0, + direction: 'asc' + } + }, + cached_fieldHourlySnapshots: { + subgraph: 'pintostalk', + queryName: 'fieldHourlySnapshots', + client: (c) => c.SG.BEANSTALK, + where: 'field: "0xd1a0d188e861ed9d15773a2f3574a2e94134ba8f"', + paginationSettings: { + field: 'season', + lastValue: 0, + direction: 'asc' + } } }; -const testIntrospect = async (sgName, c = C()) => { - const introspection = await c.SG[sgName.toUpperCase()](gql` - query IntrospectionQuery { - __schema { - queryType { - name - } - mutationType { - name - } - subscriptionType { - name - } - types { - ...FullType - } - directives { - name - description - locations - args { - ...InputValue - } - } - } - } - fragment FullType on __Type { - kind - name - description - fields(includeDeprecated: true) { - name - description - args { - ...InputValue - } - type { - ...TypeRef - } - isDeprecated - deprecationReason - } - inputFields { - ...InputValue - } - interfaces { - ...TypeRef - } - enumValues(includeDeprecated: true) { - name - description - isDeprecated - deprecationReason - } - possibleTypes { - ...TypeRef - } - } - fragment InputValue on __InputValue { - name - description - type { - ...TypeRef - } - defaultValue - } - fragment TypeRef on __Type { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - ofType { - kind - name - } - } - } - } - } - } - } - `); +const testIntrospect = async (sgName) => { + const introspection = await axios.post(`https://graph.pinto.money/${sgName}`, { + query: + 'query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description locations args { ...InputValue } } } } fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } } fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } }' + }); + + const deployment = introspection.headers['x-deployment']; + const schema = introspection.data.data.__schema; + + if ((await redis.get(`sg-deployment:${sgName}`)) === deployment) { + console.log('using cached introspection'); + return JSON.parse(await redis.get(`sg-introspection:${sgName}`)); + } // Find the underlying types for each enabled query const queryInfo = {}; - const queryTypes = introspection.__schema.types.find((t) => t.kind === 'OBJECT' && t.name === 'Query'); + const queryTypes = schema.types.find((t) => t.kind === 'OBJECT' && t.name === 'Query'); for (const field of queryTypes.fields) { - const configQuery = config[sgName].queries.find((q) => q.underlying.name === field.name); + const configQuery = Object.entries(config).find( + ([key, queryCfg]) => queryCfg.subgraph === sgName && queryCfg.queryName === field.name + ); if (configQuery) { let type = field.type; while (type.ofType) { type = type.ofType; } - queryInfo[configQuery.name] = { + queryInfo[configQuery[0]] = { type: type.name }; } @@ -229,31 +108,26 @@ const testIntrospect = async (sgName, c = C()) => { // Identify all fields accessible for each query for (const query in queryInfo) { - const queryObject = introspection.__schema.types.find( - (t) => t.kind === 'OBJECT' && t.name === queryInfo[query].type - ); + const queryObject = schema.types.find((t) => t.kind === 'OBJECT' && t.name === queryInfo[query].type); queryInfo[query].fields = queryObject.fields.map((f) => f.name); } - console.log(queryInfo); - console.log(Object.keys(queryInfo).map((k) => `{ ${k} { ${queryInfo[k].fields.join(' ')} } }`)); + // Should also save the subgraph version number and only recompute this if that changes + await redis.set(`sg-deployment:${sgName}`, deployment); + await redis.set(`sg-introspection:${sgName}`, JSON.stringify(queryInfo)); - await redis.set('introspection:beanstalk', JSON.stringify(queryInfo)); + return queryInfo; }; -const testSnap = async (c = C()) => { +const testSnap = async (queryName, introspectedInfo, c = C()) => { + const cfg = config[queryName]; + const siloHourlySnapshots = await SubgraphQueryUtil.allPaginatedSG( - c.SG.BEANSTALK, - ` - { siloHourlySnapshots { id season silo stalk depositedBDV plantedBeans roots germinatingStalk penalizedStalkConvertDown unpenalizedStalkConvertDown avgConvertDownPenalty bonusStalkConvertUp totalBdvConvertUpBonus totalBdvConvertUp beanMints plantableStalk beanToMaxLpGpPerBdvRatio cropRatio avgGrownStalkPerBdvPerSeason grownStalkPerSeason convertDownPenalty activeFarmers deltaStalk deltaDepositedBDV deltaPlantedBeans deltaRoots deltaGerminatingStalk deltaPenalizedStalkConvertDown deltaUnpenalizedStalkConvertDown deltaAvgConvertDownPenalty deltaBonusStalkConvertUp deltaTotalBdvConvertUpBonus deltaTotalBdvConvertUp deltaBeanMints deltaPlantableStalk deltaBeanToMaxLpGpPerBdvRatio deltaCropRatio deltaAvgGrownStalkPerBdvPerSeason deltaGrownStalkPerSeason deltaConvertDownPenalty deltaActiveFarmers createdAt updatedAt caseId } } - `, + cfg.client(c), + `{ ${queryName} { ${introspectedInfo[queryName].fields.join(' ')} } }`, '', - `silo: "${c.BEANSTALK}"`, - { - field: 'season', - lastValue: 0, - direction: 'asc' - } + cfg.where, + cfg.paginationSettings // TODO: here it should put in the min season as to whatever we have already retrieved ); console.log(siloHourlySnapshots.length); console.log(siloHourlySnapshots.map((s) => s.season)); @@ -261,7 +135,9 @@ const testSnap = async (c = C()) => { if (require.main === module) { (async () => { - await testIntrospect('beanstalk'); + const queryInfo = await testIntrospect('pintostalk'); + console.log(queryInfo); + console.log(Object.keys(queryInfo).map((k) => `{ ${k} { ${queryInfo[k].fields.join(' ')} } }`)); // await testSnap(); // console.log(JSON.parse(await redis.get('introspection:beanstalk'))); })(); From e9bfc3ffebf143559a45e9d81eb342a24e038b56 Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:38:51 -0500 Subject: [PATCH 05/20] Caching flow --- src/routes/sg-cache-routes.js | 59 ++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/src/routes/sg-cache-routes.js b/src/routes/sg-cache-routes.js index 7c75ce4..d61ea00 100644 --- a/src/routes/sg-cache-routes.js +++ b/src/routes/sg-cache-routes.js @@ -34,7 +34,7 @@ router.get('/', async (ctx) => { // Latest season will get rewritten always since it can be updating, and any further seasons would get appended to the cache. // Then can respond to user request for specific fields - const queryInfo = await testIntrospect(); + const queryInfo = await introspect('subgraph'); const t = await testSnap(queryName, queryInfo); const value = JSON.parse(await redis.get(key)); @@ -74,7 +74,7 @@ const config = { } }; -const testIntrospect = async (sgName) => { +const introspect = async (sgName) => { const introspection = await axios.post(`https://graph.pinto.money/${sgName}`, { query: 'query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description locations args { ...InputValue } } } } fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } } fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } }' @@ -85,7 +85,7 @@ const testIntrospect = async (sgName) => { if ((await redis.get(`sg-deployment:${sgName}`)) === deployment) { console.log('using cached introspection'); - return JSON.parse(await redis.get(`sg-introspection:${sgName}`)); + return { fromCache: true, introspection: JSON.parse(await redis.get(`sg-introspection:${sgName}`)) }; } // Find the underlying types for each enabled query @@ -116,29 +116,58 @@ const testIntrospect = async (sgName) => { await redis.set(`sg-deployment:${sgName}`, deployment); await redis.set(`sg-introspection:${sgName}`, JSON.stringify(queryInfo)); - return queryInfo; + // TODO: when from cache false, clear all underlying cache data for the corresponding subgraph + return { fromCache: false, introspection: queryInfo }; }; -const testSnap = async (queryName, introspectedInfo, c = C()) => { - const cfg = config[queryName]; +// Returns { latest: , cache: [] } +const getCachedResults = async (cachedQueryName) => { + const cfg = config[cachedQueryName]; + const cachedResults = JSON.parse(await redis.get(`sg:cache:${cachedQueryName}`)) ?? []; - const siloHourlySnapshots = await SubgraphQueryUtil.allPaginatedSG( + return { + latest: cachedResults[cachedResults.length - 1][cfg.paginationSettings.field] ?? cfg.paginationSettings.lastValue, + cache: cachedResults + }; +}; + +const queryFreshResults = async (cachedQueryName, introspection, latestValue, c = C()) => { + const cfg = config[cachedQueryName]; + return await SubgraphQueryUtil.allPaginatedSG( cfg.client(c), - `{ ${queryName} { ${introspectedInfo[queryName].fields.join(' ')} } }`, + `{ ${cfg.queryName} { ${introspection[cachedQueryName].fields.join(' ')} } }`, '', cfg.where, - cfg.paginationSettings // TODO: here it should put in the min season as to whatever we have already retrieved + { ...cfg.paginationSettings, lastValue: latestValue } ); - console.log(siloHourlySnapshots.length); - console.log(siloHourlySnapshots.map((s) => s.season)); +}; + +const aggregateAndCache = async (cachedQueryName, cachedResults, freshResults) => { + // The final element was re-retrieved and included in the fresh results. + const aggregated = [...cachedResults.slice(0, -1), ...freshResults]; + await redis.set(`sg:cache:${cachedQueryName}`, JSON.stringify(aggregated)); + return aggregated; }; if (require.main === module) { (async () => { - const queryInfo = await testIntrospect('pintostalk'); - console.log(queryInfo); - console.log(Object.keys(queryInfo).map((k) => `{ ${k} { ${queryInfo[k].fields.join(' ')} } }`)); - // await testSnap(); + const { fromCache, introspection } = await introspect('pintostalk'); + const { latest, cache } = await getCachedResults('cached_siloHourlySnapshots'); + const freshResults = await queryFreshResults('cached_siloHourlySnapshots', introspection, latest); + const aggregated = await aggregateAndCache('cached_siloHourlySnapshots', cache, freshResults); + console.log(aggregated); + + // console.log(queryInfo); + // console.log(Object.keys(queryInfo).map((k) => `{ ${k} { ${queryInfo[k].fields.join(' ')} } }`)); + // const cachedResults = await getCachedResults('cached_siloHourlySnapshots'); + // console.log(cachedResults); // console.log(JSON.parse(await redis.get('introspection:beanstalk'))); })(); } + +// full introspect +// find cached result for requested entity +// identify max season/id etc +// sg query all gte that one +// aggregate and save to cache +// return result From af453eb9cbda2680ecc4cecfa105d6475130614f Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Thu, 11 Dec 2025 14:03:10 -0500 Subject: [PATCH 06/20] Working cache --- src/routes/sg-cache-routes.js | 66 +++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/src/routes/sg-cache-routes.js b/src/routes/sg-cache-routes.js index d61ea00..dfed5fc 100644 --- a/src/routes/sg-cache-routes.js +++ b/src/routes/sg-cache-routes.js @@ -84,7 +84,6 @@ const introspect = async (sgName) => { const schema = introspection.data.data.__schema; if ((await redis.get(`sg-deployment:${sgName}`)) === deployment) { - console.log('using cached introspection'); return { fromCache: true, introspection: JSON.parse(await redis.get(`sg-introspection:${sgName}`)) }; } @@ -116,17 +115,31 @@ const introspect = async (sgName) => { await redis.set(`sg-deployment:${sgName}`, deployment); await redis.set(`sg-introspection:${sgName}`, JSON.stringify(queryInfo)); - // TODO: when from cache false, clear all underlying cache data for the corresponding subgraph return { fromCache: false, introspection: queryInfo }; }; +const clearSubgraphCache = async (subgraph) => { + let cursor = '0'; + do { + const reply = await redis.scan(cursor, { + MATCH: `sg:${subgraph}:*`, + COUNT: 100 + }); + if (reply.keys.length > 0) { + await redis.del(...reply.keys); + } + cursor = reply.cursor; + } while (cursor !== '0'); +}; + // Returns { latest: , cache: [] } const getCachedResults = async (cachedQueryName) => { const cfg = config[cachedQueryName]; - const cachedResults = JSON.parse(await redis.get(`sg:cache:${cachedQueryName}`)) ?? []; + const cachedResults = JSON.parse(await redis.get(`sg:${cfg.subgraph}:${cachedQueryName}`)) ?? []; return { - latest: cachedResults[cachedResults.length - 1][cfg.paginationSettings.field] ?? cfg.paginationSettings.lastValue, + latest: + cachedResults?.[cachedResults.length - 1]?.[cfg.paginationSettings.field] ?? cfg.paginationSettings.lastValue, cache: cachedResults }; }; @@ -143,31 +156,38 @@ const queryFreshResults = async (cachedQueryName, introspection, latestValue, c }; const aggregateAndCache = async (cachedQueryName, cachedResults, freshResults) => { + const cfg = config[cachedQueryName]; // The final element was re-retrieved and included in the fresh results. const aggregated = [...cachedResults.slice(0, -1), ...freshResults]; - await redis.set(`sg:cache:${cachedQueryName}`, JSON.stringify(aggregated)); + await redis.set(`sg:${cfg.subgraph}:${cachedQueryName}`, JSON.stringify(aggregated)); return aggregated; }; if (require.main === module) { (async () => { - const { fromCache, introspection } = await introspect('pintostalk'); - const { latest, cache } = await getCachedResults('cached_siloHourlySnapshots'); - const freshResults = await queryFreshResults('cached_siloHourlySnapshots', introspection, latest); - const aggregated = await aggregateAndCache('cached_siloHourlySnapshots', cache, freshResults); - console.log(aggregated); - - // console.log(queryInfo); - // console.log(Object.keys(queryInfo).map((k) => `{ ${k} { ${queryInfo[k].fields.join(' ')} } }`)); - // const cachedResults = await getCachedResults('cached_siloHourlySnapshots'); - // console.log(cachedResults); - // console.log(JSON.parse(await redis.get('introspection:beanstalk'))); + const QUERY_NAME = 'cached_siloHourlySnapshots'; + const sgName = config[QUERY_NAME].subgraph; + + console.time('query >9k seasons'); + + const { fromCache, introspection } = await introspect(sgName); + console.log('introspection from cache?', fromCache); + if (!fromCache) { + console.log(`New deployment detected; clearing subgraph cache for ${sgName}`); + await clearSubgraphCache(sgName); + } + + const { latest, cache } = await getCachedResults(QUERY_NAME); + console.log('latest', latest, 'cache length', cache.length); + const freshResults = await queryFreshResults(QUERY_NAME, introspection, latest); + console.log('fresh results length', freshResults.length); + const aggregated = await aggregateAndCache(QUERY_NAME, cache, freshResults); + console.log('aggregated results length', aggregated.length); + + console.timeEnd('query >9k seasons'); + + console.log(aggregated.slice(9270)); + + process.exit(0); })(); } - -// full introspect -// find cached result for requested entity -// identify max season/id etc -// sg query all gte that one -// aggregate and save to cache -// return result From 7a98acd0a4e25d3fd22f7d4f022e9d07c037896c Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Thu, 11 Dec 2025 14:26:21 -0500 Subject: [PATCH 07/20] Query includes arbitrary where --- src/routes/sg-cache-routes.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/routes/sg-cache-routes.js b/src/routes/sg-cache-routes.js index dfed5fc..04043e6 100644 --- a/src/routes/sg-cache-routes.js +++ b/src/routes/sg-cache-routes.js @@ -53,8 +53,6 @@ const config = { subgraph: 'pintostalk', queryName: 'siloHourlySnapshots', client: (c) => c.SG.BEANSTALK, - // TODO: ideally we could cache by farmers too? not sure if the ui actually uses this currently - where: 'silo: "0xd1a0d188e861ed9d15773a2f3574a2e94134ba8f"', paginationSettings: { field: 'season', lastValue: 0, @@ -65,7 +63,6 @@ const config = { subgraph: 'pintostalk', queryName: 'fieldHourlySnapshots', client: (c) => c.SG.BEANSTALK, - where: 'field: "0xd1a0d188e861ed9d15773a2f3574a2e94134ba8f"', paginationSettings: { field: 'season', lastValue: 0, @@ -133,9 +130,9 @@ const clearSubgraphCache = async (subgraph) => { }; // Returns { latest: , cache: [] } -const getCachedResults = async (cachedQueryName) => { +const getCachedResults = async (cachedQueryName, where) => { const cfg = config[cachedQueryName]; - const cachedResults = JSON.parse(await redis.get(`sg:${cfg.subgraph}:${cachedQueryName}`)) ?? []; + const cachedResults = JSON.parse(await redis.get(`sg:${cfg.subgraph}:${cachedQueryName}:${where}`)) ?? []; return { latest: @@ -144,28 +141,29 @@ const getCachedResults = async (cachedQueryName) => { }; }; -const queryFreshResults = async (cachedQueryName, introspection, latestValue, c = C()) => { +const queryFreshResults = async (cachedQueryName, where, latestValue, introspection, c = C()) => { const cfg = config[cachedQueryName]; return await SubgraphQueryUtil.allPaginatedSG( cfg.client(c), `{ ${cfg.queryName} { ${introspection[cachedQueryName].fields.join(' ')} } }`, '', - cfg.where, + where, { ...cfg.paginationSettings, lastValue: latestValue } ); }; -const aggregateAndCache = async (cachedQueryName, cachedResults, freshResults) => { +const aggregateAndCache = async (cachedQueryName, where, cachedResults, freshResults) => { const cfg = config[cachedQueryName]; // The final element was re-retrieved and included in the fresh results. const aggregated = [...cachedResults.slice(0, -1), ...freshResults]; - await redis.set(`sg:${cfg.subgraph}:${cachedQueryName}`, JSON.stringify(aggregated)); + await redis.set(`sg:${cfg.subgraph}:${cachedQueryName}:${where}`, JSON.stringify(aggregated)); return aggregated; }; if (require.main === module) { (async () => { const QUERY_NAME = 'cached_siloHourlySnapshots'; + const WHERE = 'silo: "0xd1a0d188e861ed9d15773a2f3574a2e94134ba8f"'.trim(); const sgName = config[QUERY_NAME].subgraph; console.time('query >9k seasons'); @@ -177,11 +175,11 @@ if (require.main === module) { await clearSubgraphCache(sgName); } - const { latest, cache } = await getCachedResults(QUERY_NAME); + const { latest, cache } = await getCachedResults(QUERY_NAME, WHERE); console.log('latest', latest, 'cache length', cache.length); - const freshResults = await queryFreshResults(QUERY_NAME, introspection, latest); + const freshResults = await queryFreshResults(QUERY_NAME, WHERE, latest, introspection); console.log('fresh results length', freshResults.length); - const aggregated = await aggregateAndCache(QUERY_NAME, cache, freshResults); + const aggregated = await aggregateAndCache(QUERY_NAME, WHERE, cache, freshResults); console.log('aggregated results length', aggregated.length); console.timeEnd('query >9k seasons'); From 2f7fd023e3664088220c318cc304cc244fc09cdb Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Thu, 11 Dec 2025 14:52:43 -0500 Subject: [PATCH 08/20] organize code --- src/datasources/redis-client.js | 8 ++ src/datasources/subgraph-client.js | 4 + src/repository/subgraph/cache-config.js | 25 +++++ src/repository/subgraph/common-subgraph.js | 15 +++ src/repository/subgraph/subgraph-cache.js | 91 ++++++++++++++++++ src/routes/sg-cache-routes.js | 106 ++------------------- 6 files changed, 152 insertions(+), 97 deletions(-) create mode 100644 src/datasources/redis-client.js create mode 100644 src/repository/subgraph/cache-config.js create mode 100644 src/repository/subgraph/subgraph-cache.js diff --git a/src/datasources/redis-client.js b/src/datasources/redis-client.js new file mode 100644 index 0000000..09bd707 --- /dev/null +++ b/src/datasources/redis-client.js @@ -0,0 +1,8 @@ +const { createClient } = require('redis'); + +const redisClient = createClient({ + url: 'redis://localhost:6379' +}); +redisClient.connect(); + +module.exports = redisClient; diff --git a/src/datasources/subgraph-client.js b/src/datasources/subgraph-client.js index 07a73b4..56077aa 100644 --- a/src/datasources/subgraph-client.js +++ b/src/datasources/subgraph-client.js @@ -6,6 +6,10 @@ const BASE_URL = 'https://graph.pinto.money/'; class SubgraphClients { static _clients = {}; + static baseUrl() { + return BASE_URL; + } + static named(name) { return SubgraphClients.fromUrl(BASE_URL + name); } diff --git a/src/repository/subgraph/cache-config.js b/src/repository/subgraph/cache-config.js new file mode 100644 index 0000000..8599c21 --- /dev/null +++ b/src/repository/subgraph/cache-config.js @@ -0,0 +1,25 @@ +// Must be List queries that dont require explicitly provided id (in subgraph framework, usually ending in 's') +const SG_CACHE_CONFIG = { + cached_siloHourlySnapshots: { + subgraph: 'pintostalk', + queryName: 'siloHourlySnapshots', + client: (c) => c.SG.BEANSTALK, + paginationSettings: { + field: 'season', + lastValue: 0, + direction: 'asc' + } + }, + cached_fieldHourlySnapshots: { + subgraph: 'pintostalk', + queryName: 'fieldHourlySnapshots', + client: (c) => c.SG.BEANSTALK, + paginationSettings: { + field: 'season', + lastValue: 0, + direction: 'asc' + } + } +}; + +module.exports = { SG_CACHE_CONFIG }; diff --git a/src/repository/subgraph/common-subgraph.js b/src/repository/subgraph/common-subgraph.js index 21f3a64..0381653 100644 --- a/src/repository/subgraph/common-subgraph.js +++ b/src/repository/subgraph/common-subgraph.js @@ -1,4 +1,7 @@ const { gql } = require('graphql-request'); +const SubgraphClients = require('../../datasources/subgraph-client'); +const axios = require('axios'); +const redisClient = require('../../datasources/redis-client'); class CommonSubgraphRepository { static async getMeta(client) { @@ -26,6 +29,18 @@ class CommonSubgraphRepository { versionNumber: meta.version.versionNumber }; } + + static async introspect(sgName) { + const introspection = await axios.post(`${SubgraphClients.baseUrl()}${sgName}`, { + query: + 'query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description locations args { ...InputValue } } } } fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } } fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } }' + }); + + const deployment = introspection.headers['x-deployment']; + const schema = introspection.data.data.__schema; + + return { deployment, schema }; + } } module.exports = CommonSubgraphRepository; diff --git a/src/repository/subgraph/subgraph-cache.js b/src/repository/subgraph/subgraph-cache.js new file mode 100644 index 0000000..08eeacf --- /dev/null +++ b/src/repository/subgraph/subgraph-cache.js @@ -0,0 +1,91 @@ +const { C } = require('../../constants/runtime-constants'); +const redisClient = require('../../datasources/redis-client'); +const SubgraphQueryUtil = require('../../utils/subgraph-query'); +const { SG_CACHE_CONFIG } = require('./cache-config'); +const CommonSubgraphRepository = require('./common-subgraph'); + +// Caches past season results for configured queries, enabling retrieval of the full history to be fast +class SubgraphCache { + static async introspect(sgName) { + const { deployment, schema } = await CommonSubgraphRepository.introspect(sgName); + + const fromCache = (await redisClient.get(`sg-deployment:${sgName}`)) === deployment; + + // Find the underlying types for each enabled query + const queryInfo = {}; + const queryTypes = schema.types.find((t) => t.kind === 'OBJECT' && t.name === 'Query'); + for (const field of queryTypes.fields) { + const configQuery = Object.entries(SG_CACHE_CONFIG).find( + ([key, queryCfg]) => queryCfg.subgraph === sgName && queryCfg.queryName === field.name + ); + if (configQuery) { + let type = field.type; + while (type.ofType) { + type = type.ofType; + } + queryInfo[configQuery[0]] = { + type: type.name + }; + } + } + + // Identify all fields accessible for each query + for (const query in queryInfo) { + const queryObject = schema.types.find((t) => t.kind === 'OBJECT' && t.name === queryInfo[query].type); + queryInfo[query].fields = queryObject.fields.map((f) => f.name); + } + + if (!fromCache) { + await redisClient.set(`sg-deployment:${sgName}`, deployment); + await redisClient.set(`sg-introspection:${sgName}`, JSON.stringify(queryInfo)); + } + + return { fromCache, introspection: queryInfo }; + } + + static async clear(sgName) { + let cursor = '0'; + do { + const reply = await redisClient.scan(cursor, { + MATCH: `sg:${sgName}:*`, + COUNT: 100 + }); + if (reply.keys.length > 0) { + await redisClient.del(...reply.keys); + } + cursor = reply.cursor; + } while (cursor !== '0'); + } + + // Returns { latest: , cache: [] } + static async getCachedResults(cachedQueryName, where) { + const cfg = SG_CACHE_CONFIG[cachedQueryName]; + const cachedResults = JSON.parse(await redisClient.get(`sg:${cfg.subgraph}:${cachedQueryName}:${where}`)) ?? []; + + return { + latest: + cachedResults?.[cachedResults.length - 1]?.[cfg.paginationSettings.field] ?? cfg.paginationSettings.lastValue, + cache: cachedResults + }; + } + + static async queryFreshResults(cachedQueryName, where, latestValue, introspection, c = C()) { + const cfg = SG_CACHE_CONFIG[cachedQueryName]; + return await SubgraphQueryUtil.allPaginatedSG( + cfg.client(c), + `{ ${cfg.queryName} { ${introspection[cachedQueryName].fields.join(' ')} } }`, + '', + where, + { ...cfg.paginationSettings, lastValue: latestValue } + ); + } + + static async aggregateAndCache(cachedQueryName, where, cachedResults, freshResults) { + const cfg = SG_CACHE_CONFIG[cachedQueryName]; + // The final element was re-retrieved and included in the fresh results. + const aggregated = [...cachedResults.slice(0, -1), ...freshResults]; + await redisClient.set(`sg:${cfg.subgraph}:${cachedQueryName}:${where}`, JSON.stringify(aggregated)); + return aggregated; + } +} +module.exports = SubgraphCache; diff --git a/src/routes/sg-cache-routes.js b/src/routes/sg-cache-routes.js index 04043e6..91fd3a5 100644 --- a/src/routes/sg-cache-routes.js +++ b/src/routes/sg-cache-routes.js @@ -1,8 +1,6 @@ const Router = require('koa-router'); const { createClient } = require('redis'); -const SubgraphQueryUtil = require('../utils/subgraph-query'); -const { C } = require('../constants/runtime-constants'); -const axios = require('axios'); +const SubgraphCache = require('../repository/subgraph/subgraph-cache'); const router = new Router({ prefix: '/sg-cache' @@ -48,6 +46,9 @@ router.get('/', async (ctx) => { module.exports = router; // Must be List queries that dont require explicitly provided id (in subgraph framework, usually ending in 's') +// Expand config +// Move things to better places +// Explore exposing graphql interface for the api const config = { cached_siloHourlySnapshots: { subgraph: 'pintostalk', @@ -71,95 +72,6 @@ const config = { } }; -const introspect = async (sgName) => { - const introspection = await axios.post(`https://graph.pinto.money/${sgName}`, { - query: - 'query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description locations args { ...InputValue } } } } fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } } fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } }' - }); - - const deployment = introspection.headers['x-deployment']; - const schema = introspection.data.data.__schema; - - if ((await redis.get(`sg-deployment:${sgName}`)) === deployment) { - return { fromCache: true, introspection: JSON.parse(await redis.get(`sg-introspection:${sgName}`)) }; - } - - // Find the underlying types for each enabled query - const queryInfo = {}; - const queryTypes = schema.types.find((t) => t.kind === 'OBJECT' && t.name === 'Query'); - for (const field of queryTypes.fields) { - const configQuery = Object.entries(config).find( - ([key, queryCfg]) => queryCfg.subgraph === sgName && queryCfg.queryName === field.name - ); - if (configQuery) { - let type = field.type; - while (type.ofType) { - type = type.ofType; - } - queryInfo[configQuery[0]] = { - type: type.name - }; - } - } - - // Identify all fields accessible for each query - for (const query in queryInfo) { - const queryObject = schema.types.find((t) => t.kind === 'OBJECT' && t.name === queryInfo[query].type); - queryInfo[query].fields = queryObject.fields.map((f) => f.name); - } - - // Should also save the subgraph version number and only recompute this if that changes - await redis.set(`sg-deployment:${sgName}`, deployment); - await redis.set(`sg-introspection:${sgName}`, JSON.stringify(queryInfo)); - - return { fromCache: false, introspection: queryInfo }; -}; - -const clearSubgraphCache = async (subgraph) => { - let cursor = '0'; - do { - const reply = await redis.scan(cursor, { - MATCH: `sg:${subgraph}:*`, - COUNT: 100 - }); - if (reply.keys.length > 0) { - await redis.del(...reply.keys); - } - cursor = reply.cursor; - } while (cursor !== '0'); -}; - -// Returns { latest: , cache: [] } -const getCachedResults = async (cachedQueryName, where) => { - const cfg = config[cachedQueryName]; - const cachedResults = JSON.parse(await redis.get(`sg:${cfg.subgraph}:${cachedQueryName}:${where}`)) ?? []; - - return { - latest: - cachedResults?.[cachedResults.length - 1]?.[cfg.paginationSettings.field] ?? cfg.paginationSettings.lastValue, - cache: cachedResults - }; -}; - -const queryFreshResults = async (cachedQueryName, where, latestValue, introspection, c = C()) => { - const cfg = config[cachedQueryName]; - return await SubgraphQueryUtil.allPaginatedSG( - cfg.client(c), - `{ ${cfg.queryName} { ${introspection[cachedQueryName].fields.join(' ')} } }`, - '', - where, - { ...cfg.paginationSettings, lastValue: latestValue } - ); -}; - -const aggregateAndCache = async (cachedQueryName, where, cachedResults, freshResults) => { - const cfg = config[cachedQueryName]; - // The final element was re-retrieved and included in the fresh results. - const aggregated = [...cachedResults.slice(0, -1), ...freshResults]; - await redis.set(`sg:${cfg.subgraph}:${cachedQueryName}:${where}`, JSON.stringify(aggregated)); - return aggregated; -}; - if (require.main === module) { (async () => { const QUERY_NAME = 'cached_siloHourlySnapshots'; @@ -168,18 +80,18 @@ if (require.main === module) { console.time('query >9k seasons'); - const { fromCache, introspection } = await introspect(sgName); + const { fromCache, introspection } = await SubgraphCache.introspect(sgName); console.log('introspection from cache?', fromCache); if (!fromCache) { console.log(`New deployment detected; clearing subgraph cache for ${sgName}`); - await clearSubgraphCache(sgName); + await SubgraphCache.clear(sgName); } - const { latest, cache } = await getCachedResults(QUERY_NAME, WHERE); + const { latest, cache } = await SubgraphCache.getCachedResults(QUERY_NAME, WHERE); console.log('latest', latest, 'cache length', cache.length); - const freshResults = await queryFreshResults(QUERY_NAME, WHERE, latest, introspection); + const freshResults = await SubgraphCache.queryFreshResults(QUERY_NAME, WHERE, latest, introspection); console.log('fresh results length', freshResults.length); - const aggregated = await aggregateAndCache(QUERY_NAME, WHERE, cache, freshResults); + const aggregated = await SubgraphCache.aggregateAndCache(QUERY_NAME, WHERE, cache, freshResults); console.log('aggregated results length', aggregated.length); console.timeEnd('query >9k seasons'); From 4ed35c364107ca8b45885c3dfb49f91bf06246cd Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Thu, 11 Dec 2025 15:02:13 -0500 Subject: [PATCH 09/20] Move logic to subgraph cache --- src/repository/subgraph/cache-config.js | 4 +- src/repository/subgraph/subgraph-cache.js | 64 ++++++++++++++--------- src/routes/sg-cache-routes.js | 23 ++------ 3 files changed, 45 insertions(+), 46 deletions(-) diff --git a/src/repository/subgraph/cache-config.js b/src/repository/subgraph/cache-config.js index 8599c21..5d8427e 100644 --- a/src/repository/subgraph/cache-config.js +++ b/src/repository/subgraph/cache-config.js @@ -1,6 +1,6 @@ // Must be List queries that dont require explicitly provided id (in subgraph framework, usually ending in 's') const SG_CACHE_CONFIG = { - cached_siloHourlySnapshots: { + cache_siloHourlySnapshots: { subgraph: 'pintostalk', queryName: 'siloHourlySnapshots', client: (c) => c.SG.BEANSTALK, @@ -10,7 +10,7 @@ const SG_CACHE_CONFIG = { direction: 'asc' } }, - cached_fieldHourlySnapshots: { + cache_fieldHourlySnapshots: { subgraph: 'pintostalk', queryName: 'fieldHourlySnapshots', client: (c) => c.SG.BEANSTALK, diff --git a/src/repository/subgraph/subgraph-cache.js b/src/repository/subgraph/subgraph-cache.js index 08eeacf..ec43535 100644 --- a/src/repository/subgraph/subgraph-cache.js +++ b/src/repository/subgraph/subgraph-cache.js @@ -6,7 +6,36 @@ const CommonSubgraphRepository = require('./common-subgraph'); // Caches past season results for configured queries, enabling retrieval of the full history to be fast class SubgraphCache { - static async introspect(sgName) { + static async get(cacheQueryName, where) { + const sgName = SG_CACHE_CONFIG[cacheQueryName].subgraph; + + const { fromCache, introspection } = await this._introspect(sgName); + if (!fromCache) { + console.log(`New deployment detected; clearing subgraph cache for ${sgName}`); + await this.clear(sgName); + } + + const { latest, cache } = await this._getCachedResults(cacheQueryName, where); + const freshResults = await this._queryFreshResults(cacheQueryName, where, latest, introspection); + const aggregated = await this._aggregateAndCache(cacheQueryName, where, cache, freshResults); + return aggregated; + } + + static async clear(sgName) { + let cursor = '0'; + do { + const reply = await redisClient.scan(cursor, { + MATCH: `sg:${sgName}:*`, + COUNT: 100 + }); + if (reply.keys.length > 0) { + await redisClient.del(...reply.keys); + } + cursor = reply.cursor; + } while (cursor !== '0'); + } + + static async _introspect(sgName) { const { deployment, schema } = await CommonSubgraphRepository.introspect(sgName); const fromCache = (await redisClient.get(`sg-deployment:${sgName}`)) === deployment; @@ -43,24 +72,9 @@ class SubgraphCache { return { fromCache, introspection: queryInfo }; } - static async clear(sgName) { - let cursor = '0'; - do { - const reply = await redisClient.scan(cursor, { - MATCH: `sg:${sgName}:*`, - COUNT: 100 - }); - if (reply.keys.length > 0) { - await redisClient.del(...reply.keys); - } - cursor = reply.cursor; - } while (cursor !== '0'); - } - - // Returns { latest: , cache: [] } - static async getCachedResults(cachedQueryName, where) { - const cfg = SG_CACHE_CONFIG[cachedQueryName]; - const cachedResults = JSON.parse(await redisClient.get(`sg:${cfg.subgraph}:${cachedQueryName}:${where}`)) ?? []; + static async _getCachedResults(cacheQueryName, where) { + const cfg = SG_CACHE_CONFIG[cacheQueryName]; + const cachedResults = JSON.parse(await redisClient.get(`sg:${cfg.subgraph}:${cacheQueryName}:${where}`)) ?? []; return { latest: @@ -69,22 +83,22 @@ class SubgraphCache { }; } - static async queryFreshResults(cachedQueryName, where, latestValue, introspection, c = C()) { - const cfg = SG_CACHE_CONFIG[cachedQueryName]; + static async _queryFreshResults(cacheQueryName, where, latestValue, introspection, c = C()) { + const cfg = SG_CACHE_CONFIG[cacheQueryName]; return await SubgraphQueryUtil.allPaginatedSG( cfg.client(c), - `{ ${cfg.queryName} { ${introspection[cachedQueryName].fields.join(' ')} } }`, + `{ ${cfg.queryName} { ${introspection[cacheQueryName].fields.join(' ')} } }`, '', where, { ...cfg.paginationSettings, lastValue: latestValue } ); } - static async aggregateAndCache(cachedQueryName, where, cachedResults, freshResults) { - const cfg = SG_CACHE_CONFIG[cachedQueryName]; + static async _aggregateAndCache(cacheQueryName, where, cachedResults, freshResults) { + const cfg = SG_CACHE_CONFIG[cacheQueryName]; // The final element was re-retrieved and included in the fresh results. const aggregated = [...cachedResults.slice(0, -1), ...freshResults]; - await redisClient.set(`sg:${cfg.subgraph}:${cachedQueryName}:${where}`, JSON.stringify(aggregated)); + await redisClient.set(`sg:${cfg.subgraph}:${cacheQueryName}:${where}`, JSON.stringify(aggregated)); return aggregated; } } diff --git a/src/routes/sg-cache-routes.js b/src/routes/sg-cache-routes.js index 91fd3a5..b7fbcaf 100644 --- a/src/routes/sg-cache-routes.js +++ b/src/routes/sg-cache-routes.js @@ -50,7 +50,7 @@ module.exports = router; // Move things to better places // Explore exposing graphql interface for the api const config = { - cached_siloHourlySnapshots: { + cache_siloHourlySnapshots: { subgraph: 'pintostalk', queryName: 'siloHourlySnapshots', client: (c) => c.SG.BEANSTALK, @@ -60,7 +60,7 @@ const config = { direction: 'asc' } }, - cached_fieldHourlySnapshots: { + cache_fieldHourlySnapshots: { subgraph: 'pintostalk', queryName: 'fieldHourlySnapshots', client: (c) => c.SG.BEANSTALK, @@ -74,26 +74,11 @@ const config = { if (require.main === module) { (async () => { - const QUERY_NAME = 'cached_siloHourlySnapshots'; + const QUERY_NAME = 'cache_siloHourlySnapshots'; const WHERE = 'silo: "0xd1a0d188e861ed9d15773a2f3574a2e94134ba8f"'.trim(); - const sgName = config[QUERY_NAME].subgraph; console.time('query >9k seasons'); - - const { fromCache, introspection } = await SubgraphCache.introspect(sgName); - console.log('introspection from cache?', fromCache); - if (!fromCache) { - console.log(`New deployment detected; clearing subgraph cache for ${sgName}`); - await SubgraphCache.clear(sgName); - } - - const { latest, cache } = await SubgraphCache.getCachedResults(QUERY_NAME, WHERE); - console.log('latest', latest, 'cache length', cache.length); - const freshResults = await SubgraphCache.queryFreshResults(QUERY_NAME, WHERE, latest, introspection); - console.log('fresh results length', freshResults.length); - const aggregated = await SubgraphCache.aggregateAndCache(QUERY_NAME, WHERE, cache, freshResults); - console.log('aggregated results length', aggregated.length); - + const aggregated = await SubgraphCache.get(QUERY_NAME, WHERE); console.timeEnd('query >9k seasons'); console.log(aggregated.slice(9270)); From 1de3a63c9dd641e31c244337fef6049f94c941ad Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Mon, 15 Dec 2025 14:43:09 -0500 Subject: [PATCH 10/20] Basic working graphql endpoint --- package-lock.json | 1297 ++++++++++++++++++++++++++++++++-- package.json | 3 + src/app.js | 11 +- src/routes/graphql/init.js | 14 + src/routes/graphql/schema.js | 13 + 5 files changed, 1290 insertions(+), 48 deletions(-) create mode 100644 src/routes/graphql/init.js create mode 100644 src/routes/graphql/schema.js diff --git a/package-lock.json b/package-lock.json index 744a9c8..716d71c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.0.1", "license": "ISC", "dependencies": { + "@apollo/server": "^4.12.2", + "@as-integrations/koa": "^1.1.1", "@beanstalk/contract-storage": "^1.1.1", "@koa/cors": "^5.0.0", "alchemy-sdk": "^3.4.2", @@ -16,6 +18,7 @@ "bottleneck": "^2.19.5", "dotenv": "^16.4.5", "ethers": "^6.12.1", + "graphql": "^16.12.0", "graphql-request": "^6.1.0", "koa": "^2.15.3", "koa-bodyparser": "^4.4.1", @@ -51,6 +54,284 @@ "node": ">=6.0.0" } }, + "node_modules/@apollo/cache-control-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@apollo/cache-control-types/-/cache-control-types-1.0.3.tgz", + "integrity": "sha512-F17/vCp7QVwom9eG7ToauIKdAxpSoadsJnqIfyryLFSkLSOEqu+eC5Z3N8OXcUVStuOMcNHlyraRsA6rRICu4g==", + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/protobufjs": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.2.7.tgz", + "integrity": "sha512-Lahx5zntHPZia35myYDBRuF58tlwPskwHc5CWBZC/4bMKB6siTBWwtMrkqXcsNwQiFSzSx5hKdRPUmemrEp3Gg==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.0", + "long": "^4.0.0" + }, + "bin": { + "apollo-pbjs": "bin/pbjs", + "apollo-pbts": "bin/pbts" + } + }, + "node_modules/@apollo/server": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@apollo/server/-/server-4.12.2.tgz", + "integrity": "sha512-jKRlf+sBMMdKYrjMoiWKne42Eb6paBfDOr08KJnUaeaiyWFj+/040FjVPQI7YGLfdwnYIsl1NUUqS2UdgezJDg==", + "deprecated": "Apollo Server v4 is deprecated and will transition to end-of-life on January 26, 2026. As long as you are already using a non-EOL version of Node.js, upgrading to v5 should take only a few minutes. See https://www.apollographql.com/docs/apollo-server/previous-versions for details.", + "license": "MIT", + "dependencies": { + "@apollo/cache-control-types": "^1.0.3", + "@apollo/server-gateway-interface": "^1.1.1", + "@apollo/usage-reporting-protobuf": "^4.1.1", + "@apollo/utils.createhash": "^2.0.2", + "@apollo/utils.fetcher": "^2.0.0", + "@apollo/utils.isnodelike": "^2.0.0", + "@apollo/utils.keyvaluecache": "^2.1.0", + "@apollo/utils.logger": "^2.0.0", + "@apollo/utils.usagereporting": "^2.1.0", + "@apollo/utils.withrequired": "^2.0.0", + "@graphql-tools/schema": "^9.0.0", + "@types/express": "^4.17.13", + "@types/express-serve-static-core": "^4.17.30", + "@types/node-fetch": "^2.6.1", + "async-retry": "^1.2.1", + "cors": "^2.8.5", + "express": "^4.21.1", + "loglevel": "^1.6.8", + "lru-cache": "^7.10.1", + "negotiator": "^0.6.3", + "node-abort-controller": "^3.1.1", + "node-fetch": "^2.6.7", + "uuid": "^9.0.0", + "whatwg-mimetype": "^3.0.0" + }, + "engines": { + "node": ">=14.16.0" + }, + "peerDependencies": { + "graphql": "^16.6.0" + } + }, + "node_modules/@apollo/server-gateway-interface": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@apollo/server-gateway-interface/-/server-gateway-interface-1.1.1.tgz", + "integrity": "sha512-pGwCl/po6+rxRmDMFgozKQo2pbsSwE91TpsDBAOgf74CRDPXHHtM88wbwjab0wMMZh95QfR45GGyDIdhY24bkQ==", + "deprecated": "@apollo/server-gateway-interface v1 is part of Apollo Server v4, which is deprecated and will transition to end-of-life on January 26, 2026. As long as you are already using a non-EOL version of Node.js, upgrading to v2 should take only a few minutes. See https://www.apollographql.com/docs/apollo-server/previous-versions for details.", + "license": "MIT", + "dependencies": { + "@apollo/usage-reporting-protobuf": "^4.1.1", + "@apollo/utils.fetcher": "^2.0.0", + "@apollo/utils.keyvaluecache": "^2.1.0", + "@apollo/utils.logger": "^2.0.0" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/server/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@apollo/server/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@apollo/usage-reporting-protobuf": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@apollo/usage-reporting-protobuf/-/usage-reporting-protobuf-4.1.1.tgz", + "integrity": "sha512-u40dIUePHaSKVshcedO7Wp+mPiZsaU6xjv9J+VyxpoU/zL6Jle+9zWeG98tr/+SZ0nZ4OXhrbb8SNr0rAPpIDA==", + "dependencies": { + "@apollo/protobufjs": "1.2.7" + } + }, + "node_modules/@apollo/utils.createhash": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@apollo/utils.createhash/-/utils.createhash-2.0.2.tgz", + "integrity": "sha512-UkS3xqnVFLZ3JFpEmU/2cM2iKJotQXMoSTgxXsfQgXLC5gR1WaepoXagmYnPSA7Q/2cmnyTYK5OgAgoC4RULPg==", + "license": "MIT", + "dependencies": { + "@apollo/utils.isnodelike": "^2.0.1", + "sha.js": "^2.4.11" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@apollo/utils.dropunuseddefinitions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.dropunuseddefinitions/-/utils.dropunuseddefinitions-2.0.1.tgz", + "integrity": "sha512-EsPIBqsSt2BwDsv8Wu76LK5R1KtsVkNoO4b0M5aK0hx+dGg9xJXuqlr7Fo34Dl+y83jmzn+UvEW+t1/GP2melA==", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.fetcher": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.fetcher/-/utils.fetcher-2.0.1.tgz", + "integrity": "sha512-jvvon885hEyWXd4H6zpWeN3tl88QcWnHp5gWF5OPF34uhvoR+DFqcNxs9vrRaBBSY3qda3Qe0bdud7tz2zGx1A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@apollo/utils.isnodelike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.isnodelike/-/utils.isnodelike-2.0.1.tgz", + "integrity": "sha512-w41XyepR+jBEuVpoRM715N2ZD0xMD413UiJx8w5xnAZD2ZkSJnMJBoIzauK83kJpSgNuR6ywbV29jG9NmxjK0Q==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@apollo/utils.keyvaluecache": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.keyvaluecache/-/utils.keyvaluecache-2.1.1.tgz", + "integrity": "sha512-qVo5PvUUMD8oB9oYvq4ViCjYAMWnZ5zZwEjNF37L2m1u528x5mueMlU+Cr1UinupCgdB78g+egA1G98rbJ03Vw==", + "license": "MIT", + "dependencies": { + "@apollo/utils.logger": "^2.0.1", + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@apollo/utils.keyvaluecache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@apollo/utils.logger": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.logger/-/utils.logger-2.0.1.tgz", + "integrity": "sha512-YuplwLHaHf1oviidB7MxnCXAdHp3IqYV8n0momZ3JfLniae92eYqMIx+j5qJFX6WKJPs6q7bczmV4lXIsTu5Pg==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@apollo/utils.printwithreducedwhitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.printwithreducedwhitespace/-/utils.printwithreducedwhitespace-2.0.1.tgz", + "integrity": "sha512-9M4LUXV/fQBh8vZWlLvb/HyyhjJ77/I5ZKu+NBWV/BmYGyRmoEP9EVAy7LCVoY3t8BDcyCAGfxJaLFCSuQkPUg==", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.removealiases": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.removealiases/-/utils.removealiases-2.0.1.tgz", + "integrity": "sha512-0joRc2HBO4u594Op1nev+mUF6yRnxoUH64xw8x3bX7n8QBDYdeYgY4tF0vJReTy+zdn2xv6fMsquATSgC722FA==", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.sortast": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.sortast/-/utils.sortast-2.0.1.tgz", + "integrity": "sha512-eciIavsWpJ09za1pn37wpsCGrQNXUhM0TktnZmHwO+Zy9O4fu/WdB4+5BvVhFiZYOXvfjzJUcc+hsIV8RUOtMw==", + "dependencies": { + "lodash.sortby": "^4.7.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.stripsensitiveliterals": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.stripsensitiveliterals/-/utils.stripsensitiveliterals-2.0.1.tgz", + "integrity": "sha512-QJs7HtzXS/JIPMKWimFnUMK7VjkGQTzqD9bKD1h3iuPAqLsxd0mUNVbkYOPTsDhUKgcvUOfOqOJWYohAKMvcSA==", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.usagereporting": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apollo/utils.usagereporting/-/utils.usagereporting-2.1.0.tgz", + "integrity": "sha512-LPSlBrn+S17oBy5eWkrRSGb98sWmnEzo3DPTZgp8IQc8sJe0prDgDuppGq4NeQlpoqEHz0hQeYHAOA0Z3aQsxQ==", + "dependencies": { + "@apollo/usage-reporting-protobuf": "^4.1.0", + "@apollo/utils.dropunuseddefinitions": "^2.0.1", + "@apollo/utils.printwithreducedwhitespace": "^2.0.1", + "@apollo/utils.removealiases": "2.0.1", + "@apollo/utils.sortast": "^2.0.1", + "@apollo/utils.stripsensitiveliterals": "^2.0.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, + "node_modules/@apollo/utils.withrequired": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@apollo/utils.withrequired/-/utils.withrequired-2.0.1.tgz", + "integrity": "sha512-YBDiuAX9i1lLc6GeTy1m7DGLFn/gMnvXqlalOIMjM7DeOgIacEjjfwPqb0M1CQ2v11HhR15d1NmxJoRCfrNqcA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@as-integrations/koa": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@as-integrations/koa/-/koa-1.1.1.tgz", + "integrity": "sha512-v84cVhkLUxAH9l19pajbWp/Z9ZYTzO7jkAOiY1xndTclfpXZstiWDKejZYq7xpkBtUSSAKzNyM66uox8MP9qVg==", + "license": "MIT", + "engines": { + "node": ">=16.0" + }, + "peerDependencies": { + "@apollo/server": "^4.0.0", + "koa": "^2.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1172,6 +1453,47 @@ "@ethersproject/strings": "^5.7.0" } }, + "node_modules/@graphql-tools/merge": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.4.2.tgz", + "integrity": "sha512-XbrHAaj8yDuINph+sAfuq3QCZ/tKblrTLOpirK0+CAgNlZUCHs0Fa+xtMUURgwCVThLle1AF7svJCxFizygLsw==", + "license": "MIT", + "dependencies": { + "@graphql-tools/utils": "^9.2.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/schema": { + "version": "9.0.19", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-9.0.19.tgz", + "integrity": "sha512-oBRPoNBtCkk0zbUsyP4GaIzCt8C0aCI4ycIRUL67KK5pOHljKLBBtGT+Jr6hkzA74C8Gco8bpZPe7aWFjiaK2w==", + "license": "MIT", + "dependencies": { + "@graphql-tools/merge": "^8.4.1", + "@graphql-tools/utils": "^9.2.1", + "tslib": "^2.4.0", + "value-or-promise": "^1.0.12" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/utils": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.2.1.tgz", + "integrity": "sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, "node_modules/@graphql-typed-document-node/core": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", @@ -1672,6 +1994,60 @@ "node": ">=14" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, "node_modules/@redis/bloom": { "version": "5.10.0", "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.10.0.tgz", @@ -1792,6 +2168,25 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -1800,6 +2195,30 @@ "@types/ms": "*" } }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", + "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -1809,6 +2228,12 @@ "@types/node": "*" } }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -1843,6 +2268,17 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, "node_modules/@types/ms": { "version": "0.7.34", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", @@ -1853,6 +2289,58 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==" }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -1984,6 +2472,20 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "dependencies": { + "retry": "0.13.1" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1997,6 +2499,21 @@ "node": ">= 4.0.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/axios": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", @@ -2137,6 +2654,74 @@ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/bottleneck": { "version": "2.19.5", "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", @@ -2249,15 +2834,15 @@ } }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" }, "engines": { "node": ">= 0.4" @@ -2278,6 +2863,21 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "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", @@ -2504,6 +3104,21 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, "node_modules/cookies": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", @@ -2521,6 +3136,18 @@ "resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz", "integrity": "sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -2576,9 +3203,9 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dependencies": { "ms": "^2.1.3" }, @@ -2623,6 +3250,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -2969,6 +3597,15 @@ "node": ">=4" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ethers": { "version": "6.13.2", "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.2.tgz", @@ -3078,6 +3715,111 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/ext": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", @@ -3110,7 +3852,58 @@ "to-regex-range": "^5.0.1" }, "engines": { - "node": ">=8" + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/finalhandler/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, "node_modules/find-up": { @@ -3145,6 +3938,21 @@ } } }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", @@ -3186,6 +3994,15 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -3356,10 +4173,9 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/graphql": { - "version": "16.9.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", - "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", - "peer": true, + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", + "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -3389,6 +4205,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -3577,12 +4394,33 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "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-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", @@ -3654,11 +4492,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4610,6 +5469,28 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" + }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -4697,6 +5578,15 @@ "node": ">=0.12" } }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -4724,6 +5614,18 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -4825,6 +5727,12 @@ "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "license": "MIT" + }, "node_modules/node-cron": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", @@ -4912,10 +5820,18 @@ "node": ">=8" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "engines": { "node": ">= 0.4" }, @@ -5232,6 +6148,15 @@ "node": ">=8" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -5326,6 +6251,19 @@ "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==" }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -5348,11 +6286,11 @@ ] }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -5361,39 +6299,55 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, "node_modules/raw-body/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/raw-body/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -5473,6 +6427,14 @@ "node": ">=10" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "engines": { + "node": ">= 4" + } + }, "node_modules/retry-as-promised": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz", @@ -5516,6 +6478,79 @@ "semver": "bin/semver.js" } }, + "node_modules/send": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz", + "integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/sequelize": { "version": "6.37.4", "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.4.tgz", @@ -5652,10 +6687,35 @@ "node": ">=10" } }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -5673,6 +6733,26 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5693,14 +6773,65 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -5936,6 +7067,20 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.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", @@ -6017,6 +7162,20 @@ "node": ">= 0.6" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -6099,6 +7258,15 @@ "node": ">=6.14.2" } }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -6129,6 +7297,15 @@ "node": ">= 0.10" } }, + "node_modules/value-or-promise": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz", + "integrity": "sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -6180,6 +7357,15 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -6203,6 +7389,27 @@ "node": ">= 8" } }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wkx": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", diff --git a/package.json b/package.json index 99a6291..974a602 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "author": "", "license": "ISC", "dependencies": { + "@apollo/server": "^4.12.2", + "@as-integrations/koa": "^1.1.1", "@beanstalk/contract-storage": "^1.1.1", "@koa/cors": "^5.0.0", "alchemy-sdk": "^3.4.2", @@ -38,6 +40,7 @@ "bottleneck": "^2.19.5", "dotenv": "^16.4.5", "ethers": "^6.12.1", + "graphql": "^16.12.0", "graphql-request": "^6.1.0", "koa": "^2.15.3", "koa-bodyparser": "^4.4.1", diff --git a/src/app.js b/src/app.js index 0c70edd..2f02789 100644 --- a/src/app.js +++ b/src/app.js @@ -8,6 +8,7 @@ const proxyRoutes = require('./routes/proxy-routes.js'); const seasonRoutes = require('./routes/season-routes.js'); const inflowRoutes = require('./routes/inflow-routes.js'); const sgCacheRoutes = require('./routes/sg-cache-routes.js'); +const initGraphql = require('./routes/graphql/init.js'); const Koa = require('koa'); const bodyParser = require('koa-bodyparser'); @@ -74,7 +75,10 @@ async function appStartup() { } try { await next(); // pass control to the next function specified in .use() - ctx.body = JSON.stringify(ctx.body, formatBigintDecimal); + // GraphQL handles its own JSON serialization + if (!ctx.originalUrl.includes('/graphql')) { + ctx.body = JSON.stringify(ctx.body, formatBigintDecimal); + } if (!ctx.originalUrl.includes('healthcheck')) { console.log( `${new Date().toISOString()} [success] ${ctx.method} ${ctx.originalUrl} - ${ctx.status}` @@ -126,8 +130,9 @@ async function appStartup() { ctx.body = 'healthy'; }); - app.use(router.routes()); - app.use(router.allowedMethods()); + await initGraphql(router); + + app.use(router.routes()).use(router.allowedMethods()); app.listen(3000, () => { console.log('Server running on port 3000'); diff --git a/src/routes/graphql/init.js b/src/routes/graphql/init.js new file mode 100644 index 0000000..b8cf6c5 --- /dev/null +++ b/src/routes/graphql/init.js @@ -0,0 +1,14 @@ +const { ApolloServer } = require('@apollo/server'); +const { typeDefs, resolvers } = require('./schema'); +const { koaMiddleware } = require('@as-integrations/koa'); + +const initGraphql = async (router) => { + const apollo = new ApolloServer({ + typeDefs, + resolvers + }); + await apollo.start(); + + router.all('/graphql', koaMiddleware(apollo)); +}; +module.exports = initGraphql; diff --git a/src/routes/graphql/schema.js b/src/routes/graphql/schema.js new file mode 100644 index 0000000..ca51a36 --- /dev/null +++ b/src/routes/graphql/schema.js @@ -0,0 +1,13 @@ +const typeDefs = ` + type Query { + health: String! + } +`; + +const resolvers = { + Query: { + health: () => 'oks' + } +}; + +module.exports = { typeDefs, resolvers }; From 91d6c9ae07b568f302774c4dd8e5617a860b5849 Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:28:18 -0500 Subject: [PATCH 11/20] dynamic resolver --- src/repository/subgraph/subgraph-cache.js | 13 +++-- src/routes/graphql/init.js | 3 +- src/routes/graphql/schema.js | 63 +++++++++++++++++++---- 3 files changed, 61 insertions(+), 18 deletions(-) diff --git a/src/repository/subgraph/subgraph-cache.js b/src/repository/subgraph/subgraph-cache.js index ec43535..611e037 100644 --- a/src/repository/subgraph/subgraph-cache.js +++ b/src/repository/subgraph/subgraph-cache.js @@ -9,11 +9,7 @@ class SubgraphCache { static async get(cacheQueryName, where) { const sgName = SG_CACHE_CONFIG[cacheQueryName].subgraph; - const { fromCache, introspection } = await this._introspect(sgName); - if (!fromCache) { - console.log(`New deployment detected; clearing subgraph cache for ${sgName}`); - await this.clear(sgName); - } + const introspection = await this.introspect(sgName); const { latest, cache } = await this._getCachedResults(cacheQueryName, where); const freshResults = await this._queryFreshResults(cacheQueryName, where, latest, introspection); @@ -35,7 +31,7 @@ class SubgraphCache { } while (cursor !== '0'); } - static async _introspect(sgName) { + static async introspect(sgName) { const { deployment, schema } = await CommonSubgraphRepository.introspect(sgName); const fromCache = (await redisClient.get(`sg-deployment:${sgName}`)) === deployment; @@ -65,11 +61,14 @@ class SubgraphCache { } if (!fromCache) { + console.log(`New deployment detected; clearing subgraph cache for ${sgName}`); + await this.clear(sgName); + await redisClient.set(`sg-deployment:${sgName}`, deployment); await redisClient.set(`sg-introspection:${sgName}`, JSON.stringify(queryInfo)); } - return { fromCache, introspection: queryInfo }; + return queryInfo; } static async _getCachedResults(cacheQueryName, where) { diff --git a/src/routes/graphql/init.js b/src/routes/graphql/init.js index b8cf6c5..e45ebff 100644 --- a/src/routes/graphql/init.js +++ b/src/routes/graphql/init.js @@ -1,8 +1,9 @@ const { ApolloServer } = require('@apollo/server'); -const { typeDefs, resolvers } = require('./schema'); const { koaMiddleware } = require('@as-integrations/koa'); +const GraphQLSchema = require('./schema'); const initGraphql = async (router) => { + const { typeDefs, resolvers } = await GraphQLSchema.getTypeDefsAndResolvers(); const apollo = new ApolloServer({ typeDefs, resolvers diff --git a/src/routes/graphql/schema.js b/src/routes/graphql/schema.js index ca51a36..385228b 100644 --- a/src/routes/graphql/schema.js +++ b/src/routes/graphql/schema.js @@ -1,13 +1,56 @@ -const typeDefs = ` - type Query { - health: String! - } -`; +const { SG_CACHE_CONFIG } = require('../../repository/subgraph/cache-config'); +const SubgraphCache = require('../../repository/subgraph/subgraph-cache'); + +class GraphQLSchema { + static async getTypeDefsAndResolvers() { + const subgraphNames = new Set(Object.values(SG_CACHE_CONFIG).map((config) => config.subgraph)); + for (const subgraphName of subgraphNames) { + const introspection = await SubgraphCache.introspect(subgraphName); + } + // + const typeDefs = ` + type Entity { + id: ID! + name: String! + } + type Query { + testEntity(season_gte: Int): [Entity!]! + health: String! + } + `; + + const resolvers = { + Query: Object.keys(SG_CACHE_CONFIG).reduce((acc, configKey) => { + acc[configKey] = async (_parent, { where, ...args }, _ctx) => { + const whereClause = Object.entries(args) + .map(([key, value]) => `${key}: "${value}"`) + .join(', '); + const results = await SubgraphCache.get(configKey, whereClause); + + if (args.orderBy && args.orderDirection) { + results.sort((a, b) => { + if (args.orderDirection === 'asc') { + return a[args.orderBy] - b[args.orderBy]; + } + return b[args.orderBy] - a[args.orderBy]; + }); + } + + return results.slice(args.skip ?? 0, !!args.first ? (args.skip ?? 0) + args.first : undefined); + }; + return acc; + }, {}) + // { + // testEntity: async (a, b, c) => { + // console.log(a, b, c); + // return [{ id: '1', name: 'oks' + (b.season_gte ?? 'none') }]; + // }, + // health: () => 'oks1' + // } + }; -const resolvers = { - Query: { - health: () => 'oks' + return { typeDefs, resolvers }; } -}; +} -module.exports = { typeDefs, resolvers }; +module.exports = GraphQLSchema; From f7af1ff1dca8218a0fd470361633656bb77245a1 Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Tue, 23 Dec 2025 13:52:58 -0500 Subject: [PATCH 12/20] Schema passthrough --- src/repository/subgraph/subgraph-cache.js | 18 ++++++++-- src/routes/graphql/schema.js | 40 +++++++++++++++------ src/routes/sg-cache-routes.js | 43 ----------------------- 3 files changed, 46 insertions(+), 55 deletions(-) diff --git a/src/repository/subgraph/subgraph-cache.js b/src/repository/subgraph/subgraph-cache.js index 611e037..4d042ef 100644 --- a/src/repository/subgraph/subgraph-cache.js +++ b/src/repository/subgraph/subgraph-cache.js @@ -57,7 +57,9 @@ class SubgraphCache { // Identify all fields accessible for each query for (const query in queryInfo) { const queryObject = schema.types.find((t) => t.kind === 'OBJECT' && t.name === queryInfo[query].type); - queryInfo[query].fields = queryObject.fields.map((f) => f.name); + queryInfo[query].fields = queryObject.fields.map((f) => { + return { name: f.name, typeName: this._buildTypeName(f.type) }; + }); } if (!fromCache) { @@ -71,6 +73,18 @@ class SubgraphCache { return queryInfo; } + // Recursively build a type string to use in the re-exported schema + // new Set(schema.types.flatMap((t) => t.fields?.flatMap((f) => f.type.kind))); + static _buildTypeName(type) { + if (['OBJECT', 'SCALAR', 'ENUM'].includes(type.kind)) { + return type.name; // base case + } else if (type.kind === 'NON_NULL') { + return this._buildTypeName(type.ofType) + '!'; + } else if (type.kind === 'LIST') { + return `[${this._buildTypeName(type.ofType)}]`; + } + } + static async _getCachedResults(cacheQueryName, where) { const cfg = SG_CACHE_CONFIG[cacheQueryName]; const cachedResults = JSON.parse(await redisClient.get(`sg:${cfg.subgraph}:${cacheQueryName}:${where}`)) ?? []; @@ -86,7 +100,7 @@ class SubgraphCache { const cfg = SG_CACHE_CONFIG[cacheQueryName]; return await SubgraphQueryUtil.allPaginatedSG( cfg.client(c), - `{ ${cfg.queryName} { ${introspection[cacheQueryName].fields.join(' ')} } }`, + `{ ${cfg.queryName} { ${introspection[cacheQueryName].fields.map((f) => f.name).join(' ')} } }`, '', where, { ...cfg.paginationSettings, lastValue: latestValue } diff --git a/src/routes/graphql/schema.js b/src/routes/graphql/schema.js index 385228b..996ba05 100644 --- a/src/routes/graphql/schema.js +++ b/src/routes/graphql/schema.js @@ -3,24 +3,36 @@ const SubgraphCache = require('../../repository/subgraph/subgraph-cache'); class GraphQLSchema { static async getTypeDefsAndResolvers() { - const subgraphNames = new Set(Object.values(SG_CACHE_CONFIG).map((config) => config.subgraph)); - for (const subgraphName of subgraphNames) { - const introspection = await SubgraphCache.introspect(subgraphName); + const subgraphQueries = Object.values(SG_CACHE_CONFIG).reduce((acc, next) => { + (acc[next.subgraph] ??= []).push(next.queryName); + return acc; + }, {}); + let introspection = {}; + for (const subgraphName in subgraphQueries) { + introspection = { ...introspection, ...(await SubgraphCache.introspect(subgraphName)) }; } - // + const typeDefs = ` - type Entity { - id: ID! - name: String! - } + ${Object.keys(introspection).map( + (query) => + `type ${introspection[query].type} { + ${introspection[query].fields.map((f) => `${f.name}: ${f.typeName}`).join('\n')} + }` + )} type Query { - testEntity(season_gte: Int): [Entity!]! - health: String! + ${Object.keys(introspection) + .map((query) => `${query}: [${introspection[query].type}!]!`) + .join('\n')} } `; + //type Query { + // testEntity(season_gte: Int): [Entity!]! + // health: String! + // } const resolvers = { Query: Object.keys(SG_CACHE_CONFIG).reduce((acc, configKey) => { + // Each query supports generic where clause, and order/pagination related args acc[configKey] = async (_parent, { where, ...args }, _ctx) => { const whereClause = Object.entries(args) .map(([key, value]) => `${key}: "${value}"`) @@ -54,3 +66,11 @@ class GraphQLSchema { } module.exports = GraphQLSchema; + +if (require.main === module) { + (async () => { + const { typeDefs, resolvers } = await GraphQLSchema.getTypeDefsAndResolvers(); + console.log(typeDefs); + console.log(resolvers); + })(); +} diff --git a/src/routes/sg-cache-routes.js b/src/routes/sg-cache-routes.js index b7fbcaf..159c6f8 100644 --- a/src/routes/sg-cache-routes.js +++ b/src/routes/sg-cache-routes.js @@ -1,26 +1,10 @@ const Router = require('koa-router'); -const { createClient } = require('redis'); const SubgraphCache = require('../repository/subgraph/subgraph-cache'); const router = new Router({ prefix: '/sg-cache' }); -/* Temporary area for testing redis integration */ -// Configuration for which entities should be cached -// Need some control for which results go into the permanent storage, or when results move to permanent storage -// - better to write to it each hour, or only after a certain amount builds up? -// Need secondary kv store to track what is the current latest season captured within the permanent storage (for each entity) -// I suppose entities could track on something other than season? This should also be configurable. -// Need to clear cache upon new sg version; another reason its good to put this all in sg proxy? or can inspect header. - -// -> next step: endpoint for retrieving a specific entity; underlying behavior should be to retrieve from cache and from sg - -const redis = createClient({ - url: 'redis://localhost:6379' -}); -redis.connect(); - /** * Reads a value from the cache by key * ?key: the cache key to read @@ -45,33 +29,6 @@ router.get('/', async (ctx) => { module.exports = router; -// Must be List queries that dont require explicitly provided id (in subgraph framework, usually ending in 's') -// Expand config -// Move things to better places -// Explore exposing graphql interface for the api -const config = { - cache_siloHourlySnapshots: { - subgraph: 'pintostalk', - queryName: 'siloHourlySnapshots', - client: (c) => c.SG.BEANSTALK, - paginationSettings: { - field: 'season', - lastValue: 0, - direction: 'asc' - } - }, - cache_fieldHourlySnapshots: { - subgraph: 'pintostalk', - queryName: 'fieldHourlySnapshots', - client: (c) => c.SG.BEANSTALK, - paginationSettings: { - field: 'season', - lastValue: 0, - direction: 'asc' - } - } -}; - if (require.main === module) { (async () => { const QUERY_NAME = 'cache_siloHourlySnapshots'; From 244b0393041e091cb5ef4b39ca9f60c8f62773f0 Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Tue, 23 Dec 2025 14:16:35 -0500 Subject: [PATCH 13/20] Remove unnecessary values from return schema --- src/repository/subgraph/cache-config.js | 6 ++++-- src/repository/subgraph/subgraph-cache.js | 5 ++++- src/routes/graphql/schema.js | 7 ++++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/repository/subgraph/cache-config.js b/src/repository/subgraph/cache-config.js index 5d8427e..ba34860 100644 --- a/src/repository/subgraph/cache-config.js +++ b/src/repository/subgraph/cache-config.js @@ -8,7 +8,8 @@ const SG_CACHE_CONFIG = { field: 'season', lastValue: 0, direction: 'asc' - } + }, + omitFields: ['silo'] }, cache_fieldHourlySnapshots: { subgraph: 'pintostalk', @@ -18,7 +19,8 @@ const SG_CACHE_CONFIG = { field: 'season', lastValue: 0, direction: 'asc' - } + }, + omitFields: ['field'] } }; diff --git a/src/repository/subgraph/subgraph-cache.js b/src/repository/subgraph/subgraph-cache.js index 4d042ef..b89fb6e 100644 --- a/src/repository/subgraph/subgraph-cache.js +++ b/src/repository/subgraph/subgraph-cache.js @@ -100,7 +100,10 @@ class SubgraphCache { const cfg = SG_CACHE_CONFIG[cacheQueryName]; return await SubgraphQueryUtil.allPaginatedSG( cfg.client(c), - `{ ${cfg.queryName} { ${introspection[cacheQueryName].fields.map((f) => f.name).join(' ')} } }`, + `{ ${cfg.queryName} { ${introspection[cacheQueryName].fields + .filter((f) => !cfg.omitFields?.includes(f.name)) + .map((f) => f.name) + .join(' ')} } }`, '', where, { ...cfg.paginationSettings, lastValue: latestValue } diff --git a/src/routes/graphql/schema.js b/src/routes/graphql/schema.js index 996ba05..1b1842d 100644 --- a/src/routes/graphql/schema.js +++ b/src/routes/graphql/schema.js @@ -13,10 +13,15 @@ class GraphQLSchema { } const typeDefs = ` + scalar BigInt + scalar BigDecimal ${Object.keys(introspection).map( (query) => `type ${introspection[query].type} { - ${introspection[query].fields.map((f) => `${f.name}: ${f.typeName}`).join('\n')} + ${introspection[query].fields + .filter((f) => !SG_CACHE_CONFIG[query].omitFields?.includes(f.name)) + .map((f) => `${f.name}: ${f.typeName}`) + .join('\n')} }` )} type Query { From d77053f51ea04e5dae9d350715c36541bc538540 Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Tue, 23 Dec 2025 14:37:06 -0500 Subject: [PATCH 14/20] Add query arguments --- .../postgres/startup-seeders/apy-seeder.js | 2 +- src/routes/graphql/schema.js | 14 +++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/repository/postgres/startup-seeders/apy-seeder.js b/src/repository/postgres/startup-seeders/apy-seeder.js index 8e63bbf..a998246 100644 --- a/src/repository/postgres/startup-seeders/apy-seeder.js +++ b/src/repository/postgres/startup-seeders/apy-seeder.js @@ -22,7 +22,7 @@ class ApySeeder { // Calculate and save all vapys for each season (this will take a long time for many seasons) const TAG = Concurrent.tag('apySeeder'); for (const season of missingSeasons) { - await Concurrent.run(TAG, 5, async () => { + await Concurrent.run(TAG, 3, async () => { try { await YieldService.saveSeasonalApys({ season }); } catch (e) { diff --git a/src/routes/graphql/schema.js b/src/routes/graphql/schema.js index 1b1842d..8f2d72e 100644 --- a/src/routes/graphql/schema.js +++ b/src/routes/graphql/schema.js @@ -26,23 +26,19 @@ class GraphQLSchema { )} type Query { ${Object.keys(introspection) - .map((query) => `${query}: [${introspection[query].type}!]!`) + .map( + (query) => + `${query}(where: String, orderBy: String, orderDirection: String, skip: Int, first: Int): [${introspection[query].type}!]!` + ) .join('\n')} } `; - //type Query { - // testEntity(season_gte: Int): [Entity!]! - // health: String! - // } const resolvers = { Query: Object.keys(SG_CACHE_CONFIG).reduce((acc, configKey) => { // Each query supports generic where clause, and order/pagination related args acc[configKey] = async (_parent, { where, ...args }, _ctx) => { - const whereClause = Object.entries(args) - .map(([key, value]) => `${key}: "${value}"`) - .join(', '); - const results = await SubgraphCache.get(configKey, whereClause); + const results = await SubgraphCache.get(configKey, where); if (args.orderBy && args.orderDirection) { results.sort((a, b) => { From a542d7edaf38abe51e9e0166745166de2dc90ccf Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Tue, 23 Dec 2025 15:11:30 -0500 Subject: [PATCH 15/20] Configured all snapshot entities --- src/repository/subgraph/cache-config.js | 126 ++++++++++++++++++++-- src/repository/subgraph/subgraph-cache.js | 3 + src/routes/graphql/schema.js | 3 +- 3 files changed, 121 insertions(+), 11 deletions(-) diff --git a/src/repository/subgraph/cache-config.js b/src/repository/subgraph/cache-config.js index ba34860..43d68ab 100644 --- a/src/repository/subgraph/cache-config.js +++ b/src/repository/subgraph/cache-config.js @@ -1,26 +1,132 @@ +const paginationSettings = (fieldName) => ({ + field: fieldName, + lastValue: 0, + direction: 'asc' +}); + // Must be List queries that dont require explicitly provided id (in subgraph framework, usually ending in 's') const SG_CACHE_CONFIG = { + //////// PINTOSTALK SUBGRAPH ///////// cache_siloHourlySnapshots: { subgraph: 'pintostalk', queryName: 'siloHourlySnapshots', client: (c) => c.SG.BEANSTALK, - paginationSettings: { - field: 'season', - lastValue: 0, - direction: 'asc' - }, + paginationSettings: paginationSettings('season'), omitFields: ['silo'] }, cache_fieldHourlySnapshots: { subgraph: 'pintostalk', queryName: 'fieldHourlySnapshots', client: (c) => c.SG.BEANSTALK, - paginationSettings: { - field: 'season', - lastValue: 0, - direction: 'asc' - }, + paginationSettings: paginationSettings('season'), omitFields: ['field'] + }, + cache_gaugesInfoHourlySnapshots: { + subgraph: 'pintostalk', + queryName: 'gaugesInfoHourlySnapshots', + client: (c) => c.SG.BEANSTALK, + paginationSettings: paginationSettings('season'), + omitFields: ['gaugesInfo'] + }, + cache_marketPerformanceSeasonals: { + subgraph: 'pintostalk', + queryName: 'marketPerformanceSeasonals', + client: (c) => c.SG.BEANSTALK, + paginationSettings: paginationSettings('season'), + omitFields: ['silo'] + }, + cache_podMarketplaceHourlySnapshots: { + subgraph: 'pintostalk', + queryName: 'podMarketplaceHourlySnapshots', + client: (c) => c.SG.BEANSTALK, + paginationSettings: paginationSettings('season'), + omitFields: ['podMarketplace'] + }, + cache_seasons: { + subgraph: 'pintostalk', + queryName: 'seasons', + client: (c) => c.SG.BEANSTALK, + paginationSettings: paginationSettings('season'), + omitFields: ['beanstalk'] + }, + cache_siloAssetHourlySnapshots: { + subgraph: 'pintostalk', + queryName: 'siloAssetHourlySnapshots', + client: (c) => c.SG.BEANSTALK, + paginationSettings: paginationSettings('season'), + omitFields: ['siloAsset'] + }, + cache_tractorHourlySnapshots: { + subgraph: 'pintostalk', + queryName: 'tractorHourlySnapshots', + client: (c) => c.SG.BEANSTALK, + paginationSettings: paginationSettings('season'), + omitFields: ['tractor'] + }, + cache_unripeTokenHourlySnapshots: { + subgraph: 'pintostalk', + queryName: 'unripeTokenHourlySnapshots', + client: (c) => c.SG.BEANSTALK, + paginationSettings: paginationSettings('season'), + omitFields: ['underlyingToken', 'unripeToken'] + }, + cache_whitelistTokenHourlySnapshots: { + subgraph: 'pintostalk', + queryName: 'whitelistTokenHourlySnapshots', + client: (c) => c.SG.BEANSTALK, + paginationSettings: paginationSettings('season'), + omitFields: ['token'] + }, + cache_wrappedDepositERC20HourlySnapshots: { + subgraph: 'pintostalk', + queryName: 'wrappedDepositERC20HourlySnapshots', + client: (c) => c.SG.BEANSTALK, + paginationSettings: paginationSettings('season'), + omitFields: ['siloHourlySnapshot', 'token'] + }, + //////// PINTO SUBGRAPH ///////// + cache_beanHourlySnapshots: { + subgraph: 'pinto', + queryName: 'beanHourlySnapshots', + client: (c) => c.SG.BEAN, + paginationSettings: paginationSettings('seasonNumber'), + omitFields: ['bean', 'crossEvents', 'season'] + }, + cache_farmerBalanceHourlySnapshots: { + subgraph: 'pinto', + queryName: 'farmerBalanceHourlySnapshots', + client: (c) => c.SG.BEAN, + paginationSettings: paginationSettings('seasonNumber'), + omitFields: ['farmerBalance', 'season'] + }, + cache_poolHourlySnapshots: { + subgraph: 'pinto', + queryName: 'poolHourlySnapshots', + client: (c) => c.SG.BEAN, + paginationSettings: paginationSettings('seasonNumber'), + omitFields: ['pool', 'season', 'crossEvents'] + }, + cache_tokenHourlySnapshots: { + subgraph: 'pinto', + queryName: 'tokenHourlySnapshots', + client: (c) => c.SG.BEAN, + paginationSettings: paginationSettings('seasonNumber'), + omitFields: ['token', 'season'] + }, + //////// EXCHANGE SUBGRAPH ///////// + cache_beanstalkHourlySnapshots: { + subgraph: 'exchange', + queryName: 'beanstalkHourlySnapshots', + client: (c) => c.SG.BASIN, + paginationSettings: paginationSettings('season__season'), + omitFields: ['season', 'wells'] + }, + cache_wellHourlySnapshots: { + subgraph: 'exchange', + queryName: 'wellHourlySnapshots', + client: (c) => c.SG.BASIN, + paginationSettings: paginationSettings('season__season'), + omitFields: ['season', 'well'] } }; diff --git a/src/repository/subgraph/subgraph-cache.js b/src/repository/subgraph/subgraph-cache.js index b89fb6e..13ef653 100644 --- a/src/repository/subgraph/subgraph-cache.js +++ b/src/repository/subgraph/subgraph-cache.js @@ -91,6 +91,9 @@ class SubgraphCache { return { latest: + // TODO: this is issue for basin because it does not save season__season. + // We may need to support synthetic fields (which is releavant to other omitted associations too) + // also in the _queryFreshResults, need to query like "where: {season_: {season_gt: 50}}" cachedResults?.[cachedResults.length - 1]?.[cfg.paginationSettings.field] ?? cfg.paginationSettings.lastValue, cache: cachedResults }; diff --git a/src/routes/graphql/schema.js b/src/routes/graphql/schema.js index 8f2d72e..b544417 100644 --- a/src/routes/graphql/schema.js +++ b/src/routes/graphql/schema.js @@ -15,6 +15,7 @@ class GraphQLSchema { const typeDefs = ` scalar BigInt scalar BigDecimal + scalar Bytes ${Object.keys(introspection).map( (query) => `type ${introspection[query].type} { @@ -38,7 +39,7 @@ class GraphQLSchema { Query: Object.keys(SG_CACHE_CONFIG).reduce((acc, configKey) => { // Each query supports generic where clause, and order/pagination related args acc[configKey] = async (_parent, { where, ...args }, _ctx) => { - const results = await SubgraphCache.get(configKey, where); + const results = await SubgraphCache.get(configKey, where ?? ''); if (args.orderBy && args.orderDirection) { results.sort((a, b) => { From 194854684f8c60ec39fc11e711f2e265c17ed1cd Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Tue, 30 Dec 2025 13:35:35 -0500 Subject: [PATCH 16/20] Synthetic fields and pagination on nested fields --- src/repository/subgraph/cache-config.js | 31 ++++++++++++++++++----- src/repository/subgraph/subgraph-cache.js | 17 +++++++++---- src/routes/graphql/schema.js | 6 +++++ src/utils/subgraph-query.js | 7 +++-- 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/src/repository/subgraph/cache-config.js b/src/repository/subgraph/cache-config.js index 43d68ab..fd11a2f 100644 --- a/src/repository/subgraph/cache-config.js +++ b/src/repository/subgraph/cache-config.js @@ -1,7 +1,10 @@ -const paginationSettings = (fieldName) => ({ +const paginationSettings = (fieldName, { objectField = undefined, orderBy } = {}) => ({ field: fieldName, lastValue: 0, - direction: 'asc' + direction: 'asc', + // For synthetic fields + objectField, + orderBy: orderBy ?? fieldName }); // Must be List queries that dont require explicitly provided id (in subgraph framework, usually ending in 's') @@ -118,15 +121,31 @@ const SG_CACHE_CONFIG = { subgraph: 'exchange', queryName: 'beanstalkHourlySnapshots', client: (c) => c.SG.BASIN, - paginationSettings: paginationSettings('season__season'), - omitFields: ['season', 'wells'] + paginationSettings: paginationSettings('season_: {season', { objectField: 'season', orderBy: 'season__season' }), + omitFields: ['season', 'wells'], + syntheticFields: [ + { + queryAccessor: 'season { season }', + objectAccessor: (o) => o.season.season, + objectRewritePath: 'season', + typeName: 'Int!' + } + ] }, cache_wellHourlySnapshots: { subgraph: 'exchange', queryName: 'wellHourlySnapshots', client: (c) => c.SG.BASIN, - paginationSettings: paginationSettings('season__season'), - omitFields: ['season', 'well'] + paginationSettings: paginationSettings('season_: {season', { objectField: 'season', orderBy: 'season__season' }), + omitFields: ['season', 'well'], + syntheticFields: [ + { + queryAccessor: 'season { season }', + objectAccessor: (o) => o.season.season, + objectRewritePath: 'season', + typeName: 'Int!' + } + ] } }; diff --git a/src/repository/subgraph/subgraph-cache.js b/src/repository/subgraph/subgraph-cache.js index 13ef653..426aa9f 100644 --- a/src/repository/subgraph/subgraph-cache.js +++ b/src/repository/subgraph/subgraph-cache.js @@ -91,26 +91,33 @@ class SubgraphCache { return { latest: - // TODO: this is issue for basin because it does not save season__season. - // We may need to support synthetic fields (which is releavant to other omitted associations too) - // also in the _queryFreshResults, need to query like "where: {season_: {season_gt: 50}}" - cachedResults?.[cachedResults.length - 1]?.[cfg.paginationSettings.field] ?? cfg.paginationSettings.lastValue, + cachedResults?.[cachedResults.length - 1]?.[ + cfg.paginationSettings.objectField ?? cfg.paginationSettings.field + ] ?? cfg.paginationSettings.lastValue, cache: cachedResults }; } static async _queryFreshResults(cacheQueryName, where, latestValue, introspection, c = C()) { const cfg = SG_CACHE_CONFIG[cacheQueryName]; - return await SubgraphQueryUtil.allPaginatedSG( + const results = await SubgraphQueryUtil.allPaginatedSG( cfg.client(c), `{ ${cfg.queryName} { ${introspection[cacheQueryName].fields .filter((f) => !cfg.omitFields?.includes(f.name)) + .concat(cfg.syntheticFields?.map((f) => ({ name: f.queryAccessor })) ?? []) .map((f) => f.name) .join(' ')} } }`, '', where, { ...cfg.paginationSettings, lastValue: latestValue } ); + + for (const result of results) { + for (const syntheticField of cfg.syntheticFields ?? []) { + result[syntheticField.objectRewritePath] = syntheticField.objectAccessor(result); + } + } + return results; } static async _aggregateAndCache(cacheQueryName, where, cachedResults, freshResults) { diff --git a/src/routes/graphql/schema.js b/src/routes/graphql/schema.js index b544417..8649d6c 100644 --- a/src/routes/graphql/schema.js +++ b/src/routes/graphql/schema.js @@ -21,6 +21,12 @@ class GraphQLSchema { `type ${introspection[query].type} { ${introspection[query].fields .filter((f) => !SG_CACHE_CONFIG[query].omitFields?.includes(f.name)) + .concat( + SG_CACHE_CONFIG[query].syntheticFields?.map((f) => ({ + name: f.objectRewritePath, + typeName: f.typeName + })) ?? [] + ) .map((f) => `${f.name}: ${f.typeName}`) .join('\n')} }` diff --git a/src/utils/subgraph-query.js b/src/utils/subgraph-query.js index b7c034e..7b2ca47 100644 --- a/src/utils/subgraph-query.js +++ b/src/utils/subgraph-query.js @@ -23,8 +23,11 @@ class SubgraphQueryUtil { const retval = []; while (pagination.lastValue !== undefined) { // Construct arguments for pagination - const whereClause = `{${pagination.field}${whereSuffix}: ${formatType(pagination.lastValue)}, ${where}}`; - const paginateArguments = `(${block} where: ${whereClause} first: ${PAGE_SIZE} orderBy: ${pagination.field} orderDirection: ${pagination.direction})`; + // For nested field pagination, pagination.field could be something like "season_: {season" (see basin subgraph) + const numOpenBraces = (pagination.field.match(new RegExp(`\\{`, 'g')) || []).length; + const whereClause = `{${pagination.field}${whereSuffix}: ${formatType(pagination.lastValue)}${'}'.repeat(numOpenBraces)}, ${where}}`; + + const paginateArguments = `(${block} where: ${whereClause} first: ${PAGE_SIZE} orderBy: ${pagination.orderBy ?? pagination.field} orderDirection: ${pagination.direction})`; let entityName = ''; // Add the generated arguments to the query const paginatedQuery = query.replace(/(\w+)\s{/, (match, p1) => { From 8f0ab070a8b0c171f966e600bb523acc4696654f Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Tue, 30 Dec 2025 14:42:37 -0500 Subject: [PATCH 17/20] Fix pagination on subfield --- src/repository/subgraph/cache-config.js | 15 ++++++++++++--- src/utils/subgraph-query.js | 8 +++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/repository/subgraph/cache-config.js b/src/repository/subgraph/cache-config.js index fd11a2f..0b6ac72 100644 --- a/src/repository/subgraph/cache-config.js +++ b/src/repository/subgraph/cache-config.js @@ -1,9 +1,10 @@ -const paginationSettings = (fieldName, { objectField = undefined, orderBy } = {}) => ({ +const paginationSettings = (fieldName, { objectField, objectAccessor, orderBy } = {}) => ({ field: fieldName, lastValue: 0, direction: 'asc', // For synthetic fields objectField, + objectAccessor, orderBy: orderBy ?? fieldName }); @@ -121,7 +122,11 @@ const SG_CACHE_CONFIG = { subgraph: 'exchange', queryName: 'beanstalkHourlySnapshots', client: (c) => c.SG.BASIN, - paginationSettings: paginationSettings('season_: {season', { objectField: 'season', orderBy: 'season__season' }), + paginationSettings: paginationSettings('season_: {season', { + objectField: 'season', + objectAccessor: (o) => o.season.season, + orderBy: 'season__season' + }), omitFields: ['season', 'wells'], syntheticFields: [ { @@ -136,7 +141,11 @@ const SG_CACHE_CONFIG = { subgraph: 'exchange', queryName: 'wellHourlySnapshots', client: (c) => c.SG.BASIN, - paginationSettings: paginationSettings('season_: {season', { objectField: 'season', orderBy: 'season__season' }), + paginationSettings: paginationSettings('season_: {season', { + objectField: 'season', + objectAccessor: (o) => o.season.season, + orderBy: 'season__season' + }), omitFields: ['season', 'well'], syntheticFields: [ { diff --git a/src/utils/subgraph-query.js b/src/utils/subgraph-query.js index 7b2ca47..6cf6309 100644 --- a/src/utils/subgraph-query.js +++ b/src/utils/subgraph-query.js @@ -53,7 +53,13 @@ class SubgraphQueryUtil { } } prevPageIds = pageIds; - pagination.lastValue = result[entityName][PAGE_SIZE - 1]?.[pagination.field]; + if (!result[entityName][PAGE_SIZE - 1]) { + break; + } else { + pagination.lastValue = + pagination.objectAccessor?.(result[entityName][PAGE_SIZE - 1]) ?? + result[entityName][PAGE_SIZE - 1][pagination.field]; + } } return retval; } From 9e0da2d348184c81a4c45cc6fef7422482d0796f Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Tue, 30 Dec 2025 15:10:04 -0500 Subject: [PATCH 18/20] Add redis url envvar --- src/datasources/redis-client.js | 3 ++- src/repository/subgraph/common-subgraph.js | 1 - src/utils/env.js | 10 ++++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/datasources/redis-client.js b/src/datasources/redis-client.js index 09bd707..3e0aff8 100644 --- a/src/datasources/redis-client.js +++ b/src/datasources/redis-client.js @@ -1,7 +1,8 @@ const { createClient } = require('redis'); +const EnvUtil = require('../utils/env'); const redisClient = createClient({ - url: 'redis://localhost:6379' + url: EnvUtil.getRedisUrl() }); redisClient.connect(); diff --git a/src/repository/subgraph/common-subgraph.js b/src/repository/subgraph/common-subgraph.js index 0381653..2128d6d 100644 --- a/src/repository/subgraph/common-subgraph.js +++ b/src/repository/subgraph/common-subgraph.js @@ -1,7 +1,6 @@ const { gql } = require('graphql-request'); const SubgraphClients = require('../../datasources/subgraph-client'); const axios = require('axios'); -const redisClient = require('../../datasources/redis-client'); class CommonSubgraphRepository { static async getMeta(client) { diff --git a/src/utils/env.js b/src/utils/env.js index 18e1c5f..6cd1ef3 100644 --- a/src/utils/env.js +++ b/src/utils/env.js @@ -20,6 +20,8 @@ const SG_BEANSTALK = process.env.SG_BEANSTALK?.split(',').filter((s) => s.trim() const SG_BEAN = process.env.SG_BEAN?.split(',').filter((s) => s.trim().length > 0); const SG_BASIN = process.env.SG_BASIN?.split(',').filter((s) => s.trim().length > 0); +const REDIS_URL = process.env.REDIS_URL; + const DEV_TRACTOR_SEEDER = process.env.DEV_TRACTOR_SEEDER === 'true'; const DEV_TRACTOR_RECENT = process.env.DEV_TRACTOR_RECENT === 'true'; const DEV_TRACTOR_SEEDER_START = parseInt(process.env.DEV_TRACTOR_SEEDER_START ?? '0'); @@ -43,6 +45,10 @@ if ( throw new Error(`Invalid environment configured: one subgraph name must be provided for each chain.`); } +if (!REDIS_URL) { + throw new Error('Invalid environment configured: REDIS_URL is not set.'); +} + class EnvUtil { static isChainEnabled(chain) { return EnvUtil.getEnabledChains().includes(chain); @@ -97,6 +103,10 @@ class EnvUtil { }; } + static getRedisUrl() { + return REDIS_URL; + } + static getDevTractor() { return { seeder: DEV_TRACTOR_SEEDER, From 2814bccf92df0540f03d752619ff8c30ed2b7b22 Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Tue, 30 Dec 2025 15:14:51 -0500 Subject: [PATCH 19/20] Remove test code --- src/routes/graphql/schema.js | 7 ------ src/routes/sg-cache-routes.js | 45 ----------------------------------- 2 files changed, 52 deletions(-) delete mode 100644 src/routes/sg-cache-routes.js diff --git a/src/routes/graphql/schema.js b/src/routes/graphql/schema.js index 8649d6c..7926e78 100644 --- a/src/routes/graphql/schema.js +++ b/src/routes/graphql/schema.js @@ -60,13 +60,6 @@ class GraphQLSchema { }; return acc; }, {}) - // { - // testEntity: async (a, b, c) => { - // console.log(a, b, c); - // return [{ id: '1', name: 'oks' + (b.season_gte ?? 'none') }]; - // }, - // health: () => 'oks1' - // } }; return { typeDefs, resolvers }; diff --git a/src/routes/sg-cache-routes.js b/src/routes/sg-cache-routes.js deleted file mode 100644 index 159c6f8..0000000 --- a/src/routes/sg-cache-routes.js +++ /dev/null @@ -1,45 +0,0 @@ -const Router = require('koa-router'); -const SubgraphCache = require('../repository/subgraph/subgraph-cache'); - -const router = new Router({ - prefix: '/sg-cache' -}); - -/** - * Reads a value from the cache by key - * ?key: the cache key to read - */ -router.get('/', async (ctx) => { - const queryName = ctx.query.queryName; - - // Derive the latest season number already retrieved per query. And then retreive all gte that one. - // Latest season will get rewritten always since it can be updating, and any further seasons would get appended to the cache. - // Then can respond to user request for specific fields - - const queryInfo = await introspect('subgraph'); - const t = await testSnap(queryName, queryInfo); - - const value = JSON.parse(await redis.get(key)); - - ctx.body = { - key, - value - }; -}); - -module.exports = router; - -if (require.main === module) { - (async () => { - const QUERY_NAME = 'cache_siloHourlySnapshots'; - const WHERE = 'silo: "0xd1a0d188e861ed9d15773a2f3574a2e94134ba8f"'.trim(); - - console.time('query >9k seasons'); - const aggregated = await SubgraphCache.get(QUERY_NAME, WHERE); - console.timeEnd('query >9k seasons'); - - console.log(aggregated.slice(9270)); - - process.exit(0); - })(); -} From 98c7bdb7937c0e0eb5a011e28de1cb5198397e45 Mon Sep 17 00:00:00 2001 From: PintoPirate <189064953+PintoPirate@users.noreply.github.com> Date: Tue, 30 Dec 2025 15:20:54 -0500 Subject: [PATCH 20/20] Remove defunct route --- src/app.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/app.js b/src/app.js index 2f02789..0c9ce71 100644 --- a/src/app.js +++ b/src/app.js @@ -7,7 +7,6 @@ const fieldRoutes = require('./routes/field-routes.js'); const proxyRoutes = require('./routes/proxy-routes.js'); const seasonRoutes = require('./routes/season-routes.js'); const inflowRoutes = require('./routes/inflow-routes.js'); -const sgCacheRoutes = require('./routes/sg-cache-routes.js'); const initGraphql = require('./routes/graphql/init.js'); const Koa = require('koa'); @@ -122,8 +121,6 @@ async function appStartup() { app.use(seasonRoutes.allowedMethods()); app.use(inflowRoutes.routes()); app.use(inflowRoutes.allowedMethods()); - app.use(sgCacheRoutes.routes()); - app.use(sgCacheRoutes.allowedMethods()); const router = new Router(); router.get('/healthcheck', async (ctx) => {