Skip to content

Commit 6bb6a79

Browse files
karooolisfrolicalvrs
authored
feat(store-indexer): add experimental/local SQL API endpoint (#3676)
Co-authored-by: Kevin Ingersoll <kingersoll@gmail.com> Co-authored-by: alvarius <alvarius@lattice.xyz>
1 parent 6008573 commit 6bb6a79

6 files changed

Lines changed: 299 additions & 9 deletions

File tree

.changeset/yellow-otters-swim.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@latticexyz/store-indexer": patch
3+
---
4+
5+
Added experimental SQL API endpoint `/q` to the SQLite indexer. This is only intended for local development purposes and should not be used in production.

packages/store-indexer/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,14 @@
6161
"@sentry/utils": "^7.86.0",
6262
"@trpc/client": "10.34.0",
6363
"@trpc/server": "10.34.0",
64+
"@types/koa-bodyparser": "^4.3.12",
6465
"accepts": "^1.3.8",
6566
"better-sqlite3": "^8.6.0",
6667
"debug": "^4.3.4",
6768
"dotenv": "^16.0.3",
6869
"drizzle-orm": "^0.28.5",
6970
"koa": "^2.15.4",
71+
"koa-bodyparser": "^4.4.1",
7072
"koa-compose": "^4.1.0",
7173
"postgres": "3.3.5",
7274
"prom-client": "^15.1.2",

packages/store-indexer/src/bin/sqlite-indexer.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { drizzle } from "drizzle-orm/better-sqlite3";
77
import Database from "better-sqlite3";
88
import Koa from "koa";
99
import cors from "@koa/cors";
10+
import bodyParser from "koa-bodyparser";
1011
import { createKoaMiddleware } from "trpc-koa-adapter";
1112
import { createAppRouter } from "@latticexyz/store-sync/trpc-indexer";
1213
import { chainState, schemaVersion, syncToSqlite } from "@latticexyz/store-sync/sqlite";
@@ -28,6 +29,11 @@ const env = parseEnv(
2829
z.object({
2930
SQLITE_FILENAME: z.string().default("indexer.db"),
3031
SENTRY_DSN: z.string().optional(),
32+
ENABLE_UNSAFE_QUERY_API: z
33+
.string()
34+
.optional()
35+
.default("false")
36+
.transform((val) => val === "true"),
3137
}),
3238
),
3339
);
@@ -121,6 +127,7 @@ if (env.SENTRY_DSN) {
121127
}
122128

123129
server.use(cors());
130+
server.use(bodyParser());
124131
server.use(
125132
healthcheck({
126133
isReady: () => isCaughtUp,
@@ -136,7 +143,7 @@ server.use(
136143
}),
137144
);
138145
server.use(helloWorld());
139-
server.use(apiRoutes(database));
146+
server.use(apiRoutes({ database, enableUnsafeQueryApi: env.ENABLE_UNSAFE_QUERY_API }));
140147

141148
server.use(
142149
createKoaMiddleware({
@@ -150,3 +157,12 @@ server.use(
150157

151158
server.listen({ host: env.HOST, port: env.PORT });
152159
console.log(`sqlite indexer frontend listening on http://${env.HOST}:${env.PORT}`);
160+
161+
if (env.ENABLE_UNSAFE_QUERY_API) {
162+
console.warn("\n\n⚠️ SECURITY WARNING ⚠️");
163+
console.warn("=========================\n");
164+
console.warn("UNSAFE QUERY API IS ENABLED");
165+
console.warn("DO NOT USE IN PRODUCTION");
166+
console.warn("This will expose your database to public access");
167+
console.warn("\n=========================\n\n");
168+
}

packages/store-indexer/src/koa-middleware/sentry.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ export function errorHandler(): Koa.Middleware {
1212
} catch (err) {
1313
Sentry.withScope((scope) => {
1414
scope.addEventProcessor((event) => {
15-
return Sentry.addRequestDataToEvent(event, ctx.request);
15+
return Sentry.addRequestDataToEvent(event, {
16+
...ctx.request,
17+
body: ctx.request.body as string | Record<string, unknown> | undefined,
18+
query: ctx.request.query as Record<string, unknown> | undefined,
19+
});
1620
});
1721
Sentry.captureException(err);
1822
});
@@ -27,10 +31,10 @@ export function requestHandler(): Koa.Middleware {
2731
const hub = Sentry.getCurrentHub();
2832
hub.configureScope((scope) =>
2933
scope.addEventProcessor((event) =>
30-
Sentry.addRequestDataToEvent(event, ctx.request, {
31-
include: {
32-
user: false,
33-
},
34+
Sentry.addRequestDataToEvent(event, {
35+
...ctx.request,
36+
body: ctx.request.body as string | Record<string, unknown> | undefined,
37+
query: ctx.request.query as Record<string, unknown> | undefined,
3438
}),
3539
),
3640
);

packages/store-indexer/src/sqlite/apiRoutes.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";
2+
import { sql } from "drizzle-orm";
13
import { Middleware } from "koa";
24
import Router from "@koa/router";
35
import compose from "koa-compose";
@@ -7,10 +9,14 @@ import { debug } from "../debug";
79
import { createBenchmark } from "@latticexyz/common";
810
import { compress } from "../koa-middleware/compress";
911
import { getTablesWithRecords } from "./getTablesWithRecords";
10-
import { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";
1112

12-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
13-
export function apiRoutes(database: BaseSQLiteDatabase<"sync", any>): Middleware {
13+
type Props = {
14+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
15+
database: BaseSQLiteDatabase<"sync", any>;
16+
enableUnsafeQueryApi?: boolean;
17+
};
18+
19+
export function apiRoutes({ database, enableUnsafeQueryApi = false }: Props): Middleware {
1420
const router = new Router();
1521

1622
router.get("/api/logs", compress(), async (ctx) => {
@@ -44,5 +50,50 @@ export function apiRoutes(database: BaseSQLiteDatabase<"sync", any>): Middleware
4450
}
4551
});
4652

53+
router.post("/q", async (ctx) => {
54+
if (!enableUnsafeQueryApi) {
55+
ctx.status = 404;
56+
ctx.body = JSON.stringify({ error: "Query endpoint is not enabled" });
57+
return;
58+
}
59+
60+
try {
61+
const queries = Array.isArray(ctx.request.body) ? ctx.request.body : [];
62+
if (queries.length === 0) {
63+
ctx.status = 400;
64+
ctx.body = JSON.stringify({ error: "No queries provided" });
65+
return;
66+
}
67+
68+
const result = [];
69+
for (const { query } of queries) {
70+
const data = database.all(sql.raw(query)) as Record<string, unknown>[];
71+
if (!data || !Array.isArray(data)) {
72+
throw new Error("Invalid query result");
73+
}
74+
75+
if (data.length === 0) {
76+
result.push([]);
77+
continue;
78+
}
79+
80+
if (!data[0]) {
81+
throw new Error("Invalid row data");
82+
}
83+
84+
const columns = Object.keys(data[0]).map((key) => key.replaceAll("_", "").toLowerCase());
85+
const rows = data.map((row) => Object.values(row).map((value) => value?.toString() ?? ""));
86+
result.push([columns, ...rows]);
87+
}
88+
89+
ctx.status = 200;
90+
ctx.body = JSON.stringify({ result });
91+
} catch (error) {
92+
const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
93+
ctx.status = 400;
94+
ctx.body = JSON.stringify({ error: errorMessage });
95+
}
96+
});
97+
4798
return compose([router.routes(), router.allowedMethods()]) as Middleware;
4899
}

0 commit comments

Comments
 (0)