Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/indexer/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
testMatch: ["**/*.test.ts"],
testMatch: ["**/*.test.ts", "**/*.spec.ts"],
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
"^ponder:schema$": "<rootDir>/ponder.schema.ts",
},
};
6 changes: 5 additions & 1 deletion apps/indexer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"typecheck": "tsc --noEmit",
"test": "jest",
"test:watch": "jest --watch",
"clean": "rm -rf node_modules generated .ponder dump *.tsbuildinfo"
"clean": "rm -rf node_modules generated .ponder dump *.tsbuildinfo",
"schema": "tsx scripts/generate-api-schema.ts",
"schema:watch": "tsx scripts/watch-schema.ts"
},
"dependencies": {
"@hono/zod-openapi": "^0.19.6",
Expand All @@ -27,6 +29,7 @@
"zod-validation-error": "^3.4.1"
},
"devDependencies": {
"@electric-sql/pglite": "0.2.13",
"@types/jest": "^29.5.14",
"@types/node": "^20.16.5",
"@types/pg": "^8.11.10",
Expand All @@ -39,6 +42,7 @@
"prettier": "^3.5.3",
"ts-jest": "^29.4.1",
"ts-node": "^10.9.2",
"tsx": "^4.21.0",
"typescript": "^5.8.3"
},
"engines": {
Expand Down
151 changes: 151 additions & 0 deletions apps/indexer/scripts/generate-api-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import * as fs from "fs";
import * as path from "path";
import { execSync } from "child_process";

const PONDER_SCHEMA_PATH = path.join(__dirname, "../ponder.schema.ts");
const API_SCHEMA_PATH = path.join(__dirname, "../src/api/schema/generated.ts");

function generateApiSchema(): void {
console.log("🔄 Generating API schema from Ponder schema...");

let content = fs.readFileSync(PONDER_SCHEMA_PATH, "utf-8");

// Replace imports from ponder
content = content.replace(/import\s*{[^}]*}\s*from\s*["']ponder["'];?/g, "");

// Replace onchainTable with pgTable
content = content.replace(/onchainTable\(/g, "pgTable(");

// Replace onchainEnum with pgEnum
content = content.replace(/onchainEnum\(/g, "pgEnum(");

// Replace drizzle parameter - but NOT in strings
// Replace (drizzle) => with (t) =>
content = content.replace(/\(drizzle\)\s*=>/g, "(t) =>");
// Replace drizzle. with t. (property access)
content = content.replace(/\bdrizzle\./g, "t.");

// Handle bigint - add mode: "bigint" to all bigint columns
content = content.replace(/\.bigint\(\)/g, '.bigint({ mode: "bigint" })');
content = content.replace(
/\.bigint\(["']([^"']+)["']\)/g,
'.bigint("$1", { mode: "bigint" })',
);

// Convert composite primary keys and indexes from object to array syntax
content = content.replace(
/\(table\)\s*=>\s*\(\{\s*([\s\S]*?)\s*\}\),?\s*\);/g,
(match, objectContent) => {
const lines: string[] = [];
let depth = 0;
let currentLine = "";
let inPropertyName = true;

for (let i = 0; i < objectContent.length; i++) {
const char = objectContent[i];

if (char === ":" && depth === 0 && inPropertyName) {
inPropertyName = false;
currentLine = "";
continue;
}

if (char === "{" || char === "[") depth++;
if (char === "}" || char === "]") depth--;

if (char === "," && depth === 0) {
if (currentLine.trim()) {
lines.push(currentLine.trim());
}
currentLine = "";
inPropertyName = true;
continue;
}

if (!inPropertyName) {
currentLine += char;
}
}

if (currentLine.trim()) {
lines.push(currentLine.trim());
}

const values = lines.join(",\n ");
return `(table) => [\n ${values}\n]);`;
},
);

// Remove all relation exports
content = content.replace(
/export\s+const\s+\w+Relations\s*=\s*relations\([^;]+\);/gs,
"",
);

// Remove relation-related comments
content = content.replace(/\/\/.*relations?\s*$/gim, "");

// Clean up multiple empty lines
content = content.replace(/\n{3,}/g, "\n\n");

// Add new imports at the top (AFTER all replacements to avoid corruption)
const newImports = `import { pgTable, pgEnum, text, integer, bigint, boolean, json, index, primaryKey } from "drizzle-orm/pg-core";
`;

// Find the position after existing imports
const importMatch = content.match(/import[^;]+;/g);
if (importMatch && importMatch.length > 0) {
const lastImport = importMatch[importMatch.length - 1]!;
const lastImportIndex = content.lastIndexOf(lastImport);
const insertPosition = content.indexOf(";", lastImportIndex) + 1;
content =
content.slice(0, insertPosition) +
"\n" +
newImports +
content.slice(insertPosition);
} else {
content = newImports + "\n" + content;
}

// Add warning comment at the top
const warning = `/**
* ⚠️ AUTO-GENERATED FILE - DO NOT EDIT MANUALLY
*
* This file is automatically generated from ponder.schema.ts
* Run 'npm run generate:schema' to regenerate
*
* Last generated: ${new Date().toISOString()}
*/

`;

content = warning + content;

// Ensure directory exists
const dir = path.dirname(API_SCHEMA_PATH);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}

// Write the file
fs.writeFileSync(API_SCHEMA_PATH, content, "utf-8");

console.log("✅ API schema generated successfully at:", API_SCHEMA_PATH);

// Run eslint --fix on the generated file
try {
console.log("🔧 Running eslint --fix on generated file...");
execSync(`npx eslint "${API_SCHEMA_PATH}" --fix`, { stdio: "inherit" });
console.log("✅ Linting completed");
} catch (error) {
console.warn("⚠️ Eslint not available or failed, skipping lint step");
}
}

// Run the function
try {
generateApiSchema();
} catch (error) {
console.error("❌ Error generating API schema:", error);
process.exit(1);
}
15 changes: 15 additions & 0 deletions apps/indexer/scripts/watch-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import fs from "fs";
import path from "path";
import { execSync } from "child_process";

const PONDER_SCHEMA_PATH = path.join(__dirname, "../ponder.schema.ts");

console.log("👀 Watching ponder.schema.ts for changes...");

fs.watch(PONDER_SCHEMA_PATH, (eventType) => {
if (eventType === "change") {
console.log("📝 ponder.schema.ts changed, regenerating API schema...");
execSync("npm run schema", { stdio: "inherit" });
console.log("✅ API schema updated");
}
});
8 changes: 3 additions & 5 deletions apps/indexer/src/api/controllers/last-update/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { OpenAPIHono as Hono, createRoute, z } from "@hono/zod-openapi";
import { ChartType } from "@/api/mappers/";

import { ChartType } from "@/api/mappers";
import { LastUpdateService } from "@/api/services";
import { LastUpdateRepositoryImpl } from "@/api/repositories";

export function lastUpdate(app: Hono) {
const repository = new LastUpdateRepositoryImpl();
const service = new LastUpdateService(repository);
export function lastUpdate(app: Hono, service: LastUpdateService) {
app.openapi(
createRoute({
method: "get",
Expand Down
30 changes: 30 additions & 0 deletions apps/indexer/src/api/database/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type * as schema from "ponder:schema";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import type { PgliteDatabase } from "drizzle-orm/pglite";

/**
* Full Drizzle database type with write capabilities
* This follows Ponder's Drizzle type definition pattern from:
* node_modules/ponder/src/types/db.ts
*
* Supports:
* - NodePgDatabase: PostgreSQL via node-postgres driver
* - PgliteDatabase: PGlite embedded PostgreSQL
*/
export type Drizzle =
| NodePgDatabase<typeof schema>
| PgliteDatabase<typeof schema>;

/**
* Read-only Drizzle database type (used in Ponder API context)
* Omits write operations: insert, update, delete, transaction
*/
export type ReadonlyDrizzle = Omit<
Drizzle,
| "insert"
| "update"
| "delete"
| "transaction"
| "refreshMaterializedView"
| "_"
>;
48 changes: 31 additions & 17 deletions apps/indexer/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { db } from "ponder:api";
import { graphql } from "ponder";
import { db } from "ponder:api";
import { OpenAPIHono as Hono } from "@hono/zod-openapi";
import schema from "ponder:schema";
import { logger } from "hono/logger";
Expand Down Expand Up @@ -38,6 +38,8 @@ import {
DrizzleProposalsActivityRepository,
NounsVotingPowerRepository,
AccountInteractionsRepository,
LastUpdateRepositoryImpl,
ProposalsRepository,
} from "@/api/repositories";
import { errorHandler } from "@/api/middlewares";
import { getClient } from "@/lib/client";
Expand All @@ -55,6 +57,7 @@ import {
BalanceVariationsService,
HistoricalBalancesService,
DaoService,
LastUpdateService,
} from "@/api/services";
import { CONTRACT_ADDRESSES } from "@/lib/constants";
import { DaoIdEnum } from "@/lib/enums";
Expand Down Expand Up @@ -105,29 +108,31 @@ const optimisticProposalType =
? daoConfig.optimisticProposalType
: undefined;

const repo = new DrizzleRepository();
const votingPowerRepo = new VotingPowerRepository();
const proposalsRepo = new DrizzleProposalsActivityRepository();
const transactionsRepo = new TransactionsRepository();
const delegationPercentageRepo = new DelegationPercentageRepository();
const repo = new DrizzleRepository(db);
const votingPowerRepo = new VotingPowerRepository(db);

const delegationPercentageService = new DelegationPercentageService(
delegationPercentageRepo,
new DelegationPercentageRepository(db),
);
const accountBalanceRepo = new AccountBalanceRepository(db);

const transactionsService = new TransactionsService(
new TransactionsRepository(db),
);
const accountBalanceRepo = new AccountBalanceRepository();
const accountInteractionRepo = new AccountInteractionsRepository();
const transactionsService = new TransactionsService(transactionsRepo);
const votingPowerService = new VotingPowerService(
env.DAO_ID === DaoIdEnum.NOUNS
? new NounsVotingPowerRepository()
? new NounsVotingPowerRepository(db)
: votingPowerRepo,
votingPowerRepo,
);
const daoCache = new DaoCache();
const daoService = new DaoService(daoClient, daoCache, env.CHAIN_ID);
const accountBalanceService = new BalanceVariationsService(
accountBalanceRepo,
accountInteractionRepo,
new AccountInteractionsRepository(db),
);
const repository = new LastUpdateRepositoryImpl(db);
const lastUpdateService = new LastUpdateService(repository);

if (env.DUNE_API_URL && env.DUNE_API_KEY) {
const duneClient = new DuneService(env.DUNE_API_URL, env.DUNE_API_KEY);
Expand All @@ -137,7 +142,7 @@ if (env.DUNE_API_URL && env.DUNE_API_KEY) {
const tokenPriceClient =
env.DAO_ID === DaoIdEnum.NOUNS
? new NFTPriceService(
new NFTPriceRepository(),
new NFTPriceRepository(db),
env.COINGECKO_API_URL,
env.COINGECKO_API_KEY,
)
Expand All @@ -151,16 +156,25 @@ tokenHistoricalData(app, tokenPriceClient);
token(
app,
tokenPriceClient,
new TokenService(new TokenRepository()),
new TokenService(new TokenRepository(db)),
env.DAO_ID,
);

tokenDistribution(app, repo);
governanceActivity(app, repo, tokenType);
proposalsActivity(app, proposalsRepo, env.DAO_ID, daoClient);
proposalsActivity(
app,
new DrizzleProposalsActivityRepository(db),
env.DAO_ID,
daoClient,
);
proposals(
app,
new ProposalsService(repo, daoClient, optimisticProposalType),
new ProposalsService(
new ProposalsRepository(db),
daoClient,
optimisticProposalType,
),
daoClient,
blockTime,
);
Expand All @@ -171,7 +185,7 @@ historicalBalances(
new HistoricalBalancesService(accountBalanceRepo),
);
transactions(app, transactionsService);
lastUpdate(app);
lastUpdate(app, lastUpdateService);
delegationPercentage(app, delegationPercentageService);
votingPower(app, votingPowerService);
votingPowerVariations(app, votingPowerService);
Expand Down
Loading
Loading