diff --git a/.env.example b/.env.example index 5ffe6ea8..184a77e2 100644 --- a/.env.example +++ b/.env.example @@ -7,3 +7,6 @@ IPFS_GATEWAY= SENTRY_DSN= STORAGE_DIR=./data CACHE_DIR=./.cache +DEPLOYMENT_ENVIRONMENT=local +PORT=8080 +LOG_LEVEL=debug diff --git a/fly.production.toml b/fly.production.toml index 1e7c16d4..bb06baa6 100644 --- a/fly.production.toml +++ b/fly.production.toml @@ -8,6 +8,8 @@ kill_timeout = 5 [env] PORT = "8080" + DEPLOYMENT_ENVIRONMENT = "production" + LOG_LEVEL = "info" STORAGE_DIR = "/mnt/indexer/data" CACHE_DIR = "/mnt/indexer/cache" diff --git a/fly.staging.toml b/fly.staging.toml index a168767e..d5f68793 100644 --- a/fly.staging.toml +++ b/fly.staging.toml @@ -8,6 +8,8 @@ kill_timeout = 5 [env] PORT = "8080" + DEPLOYMENT_ENVIRONMENT = "staging" + LOG_LEVEL = "debug" STORAGE_DIR = "/mnt/indexer/data" CACHE_DIR = "/mnt/indexer/cache" diff --git a/package-lock.json b/package-lock.json index 5c370001..26e49a5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,8 @@ "pluralistic": "github:gitcoinco/pluralistic.js#644d14fff65100f005d7afc18799b0f99b72ae24", "serve-index": "^1.9.1", "statuses-bitmap": "github:gitcoinco/statuses-bitmap#3d8fd370f209ccbaffd3781cf2b6d2895237c21c", - "write-file-atomic": "^5.0.1" + "write-file-atomic": "^5.0.1", + "zod": "^3.21.4" }, "devDependencies": { "@types/cors": "^2.8.13", @@ -7026,6 +7027,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { @@ -11608,6 +11617,11 @@ }, "yocto-queue": { "version": "0.1.0" + }, + "zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==" } } } diff --git a/package.json b/package.json index f3b2afa3..3950aa9f 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "pluralistic": "github:gitcoinco/pluralistic.js#644d14fff65100f005d7afc18799b0f99b72ae24", "serve-index": "^1.9.1", "statuses-bitmap": "github:gitcoinco/statuses-bitmap#3d8fd370f209ccbaffd3781cf2b6d2895237c21c", - "write-file-atomic": "^5.0.1" + "write-file-atomic": "^5.0.1", + "zod": "^3.21.4" }, "devDependencies": { "@types/cors": "^2.8.13", diff --git a/src/config.ts b/src/config.ts index acb58eb6..30166f19 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,7 +1,7 @@ import "dotenv/config"; import { parseArgs } from "node:util"; import { ToBlock } from "chainsauce"; -import path from "node:path"; +import { z } from "zod"; type ChainId = number; @@ -352,7 +352,6 @@ export type Config = { passportApiKey: string; cacheDir: string | null; logLevel: "trace" | "debug" | "info" | "warn" | "error"; - clear: boolean; ipfsGateway: string; coingeckoApiKey: string | null; coingeckoApiUrl: string; @@ -360,27 +359,39 @@ export type Config = { runOnce: boolean; apiHttpPort: number; sentryDsn: string | null; + deploymentEnvironment: "local" | "development" | "staging" | "production"; }; export function getConfig(): Config { - const apiHttpPort = Number(process.env.PORT || "4000"); + const apiHttpPort = z.coerce.number().parse(process.env.PORT); - if (!process.env.PASSPORT_SCORER_ID) { - throw new Error("PASSPORT_SCORER_ID is not set"); - } - if (!process.env.PASSPORT_API_KEY) { - throw new Error("PASSPORT_SCORER_ID is not set"); - } - const passportScorerId = process.env.PASSPORT_SCORER_ID; - const passportApiKey = process.env.PASSPORT_API_KEY; + const deploymentEnvironment = z + .union([ + z.literal("local"), + z.literal("development"), + z.literal("staging"), + z.literal("production"), + ]) + .parse(process.env.DEPLOYMENT_ENVIRONMENT); + + const passportScorerId = z.string().parse(process.env.PASSPORT_SCORER_ID); + + const passportApiKey = z.string().parse(process.env.PASSPORT_API_KEY); - const coingeckoApiKey = process.env.COINGECKO_API_KEY ?? null; + const coingeckoApiKey = z + .union([z.string(), z.null()]) + .default(null) + .parse(process.env.COINGECKO_API_KEY); - const coingeckoApiUrl = process.env.COINGECKO_API_KEY - ? "https://pro-api.coingecko.com/api/v3/" - : "https://api.coingecko.com/api/v3"; + const coingeckoApiUrl = + coingeckoApiKey === null + ? "https://api.coingecko.com/api/v3" + : "https://pro-api.coingecko.com/api/v3/"; - const storageDir = path.join(process.env.STORAGE_DIR || "./data"); + const storageDir = z + .string() + .default("./data") + .parse(process.env.STORAGE_DIR); const { values: args } = parseArgs({ options: { @@ -399,9 +410,6 @@ export function getConfig(): Config { "run-once": { type: "boolean", }, - clear: { - type: "boolean", - }, "no-cache": { type: "boolean", }, @@ -420,37 +428,40 @@ export function getConfig(): Config { return c; }); - const toBlock = - "to-block" in args - ? args["to-block"] === "latest" - ? ("latest" as const) - : Number(args["to-block"]) - : ("latest" as const); + const toBlock = z + .union([z.coerce.number(), z.literal("latest")]) + .default("latest") + .parse(args["to-block"]); - const fromBlock = "from-block" in args ? Number(args["from-block"]) : 0; - - const logLevel = args["log-level"] ?? "info"; - if ( - logLevel !== "trace" && - logLevel !== "debug" && - logLevel !== "info" && - logLevel !== "warn" && - logLevel !== "error" - ) { - throw new Error(`Invalid log level: ${logLevel}`); - } + const fromBlock = z.coerce.number().default(0).parse(args["from-block"]); - const clear = args.clear ?? false; + const logLevel = z + .union([ + z.literal("trace"), + z.literal("debug"), + z.literal("info"), + z.literal("warn"), + z.literal("error"), + ]) + .default("info") + .parse(process.env.LOG_LEVEL); - const runOnce = args["run-once"] ?? false; + const runOnce = z.boolean().default(false).parse(args["run-once"]); - const cacheDir = args["no-cache"] - ? null - : process.env.CACHE_DIR || "./.cache"; + const cacheDir = z + .union([z.string(), z.null()]) + .default("./.cache") + .parse(process.env.CACHE_DIR); - const ipfsGateway = process.env.IPFS_GATEWAY || "https://cloudflare-ipfs.com"; + const ipfsGateway = z + .string() + .default("https://cloudflare-ipfs.com") + .parse(process.env.IPFS_GATEWAY); - const sentryDsn = process.env.SENTRY_DSN ?? null; + const sentryDsn = z + .union([z.string(), z.null()]) + .default(null) + .parse(process.env.SENTRY_DSN); return { sentryDsn, @@ -463,10 +474,10 @@ export function getConfig(): Config { cacheDir, logLevel, runOnce, - clear, ipfsGateway, passportApiKey, passportScorerId, apiHttpPort, + deploymentEnvironment, }; } diff --git a/src/index.ts b/src/index.ts index 7378e815..657992cb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -32,10 +32,12 @@ async function main(): Promise { }); } - const baseLogger = pino({ level: "trace" }).child({ - service: "indexer-staging", // XXX TODO grab environment name from env variable + const baseLogger = pino({ level: config.logLevel }).child({ + service: `indexer-${config.deploymentEnvironment}`, }); + // Promise will be resolved once the catchup is done. Afterwards, services + // will still be in listen-and-update mode await Promise.all([ catchupAndWatchPassport({ ...config,