feat: add zero

This commit is contained in:
Max Koon
2025-10-13 21:10:46 -04:00
parent b4d13e6a9f
commit 92d297e2c9
36 changed files with 3318 additions and 455 deletions

17
api/package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "@money/api",
"type": "module",
"private": true,
"scripts": {
"dev": "tsx watch src/index.ts"
},
"dependencies": {
"@hono/node-server": "^1.19.5",
"@money/shared": "link:../shared",
"better-auth": "^1.3.27",
"hono": "^4.9.12"
},
"devDependencies": {
"@types/node": "^24.7.2"
}
}

25
api/src/auth.ts Normal file
View File

@@ -0,0 +1,25 @@
import { betterAuth } from "better-auth";
import { genericOAuth } from "better-auth/plugins";
import { expo } from "@better-auth/expo";
import { Pool } from "pg";
export const auth = betterAuth({
database: new Pool({
connectionString: process.env.DATABASE_URL,
}),
trustedOrigins: ["money://", "http://localhost:8081"],
plugins: [
expo(),
genericOAuth({
config: [
{
providerId: 'koon-family',
clientId: process.env.OAUTH_CLIENT_ID!,
clientSecret: process.env.OAUTH_CLIENT_SECRET!,
discoveryUrl: process.env.OAUTH_DISCOVERY_URL!,
scopes: ["profile", "email"],
}
]
})
]
});

5
api/src/hono.ts Normal file
View File

@@ -0,0 +1,5 @@
import type { AuthData } from "@money/shared/auth";
import { Hono } from "hono";
export const getHono = () =>
new Hono<{ Variables: { auth: AuthData | null } }>();

55
api/src/index.ts Normal file
View File

@@ -0,0 +1,55 @@
import { serve } from "@hono/node-server";
import { authDataSchema } from "@money/shared/auth";
import { cors } from "hono/cors";
import { auth } from "./auth";
import { getHono } from "./hono";
import { zero } from "./zero";
const app = getHono();
app.use(
"/api/*",
cors({
origin: (origin) => origin ?? "",
allowMethods: ["POST", "GET", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization"],
credentials: true,
}),
);
app.on(["GET", "POST"], "/api/auth/*", (c) => auth.handler(c.req.raw));
app.use("*", async (c, next) => {
const authHeader = c.req.raw.headers.get("Authorization");
const cookie = authHeader?.split("Bearer ")[1];
const newHeaders = new Headers(c.req.raw.headers);
if (cookie) {
newHeaders.set("Cookie", cookie);
}
const session = await auth.api.getSession({ headers: newHeaders });
if (!session) {
c.set("auth", null);
return next();
}
c.set("auth", authDataSchema.parse(session));
return next();
});
app.route("/api/zero", zero);
app.get("/api", (c) => c.text("OK"));
app.get("/", (c) => c.text("OK"));
serve(
{
fetch: app.fetch,
port: 3000,
},
(info) => {
console.log(`Server is running on ${info.address}:${info.port}`);
},
);

96
api/src/zero.ts Normal file
View File

@@ -0,0 +1,96 @@
import {
type ReadonlyJSONValue,
type ServerTransaction,
withValidation,
} from "@rocicorp/zero";
import {
handleGetQueriesRequest,
PushProcessor,
ZQLDatabase,
} from "@rocicorp/zero/server";
import {
// createMutators as createMutatorsShared,
// isLoggedIn,
// type Mutators,
queries,
schema,
type Schema,
} from "@money/shared";
import type { AuthData } from "@money/shared/auth";
// import { auditLogs } from "@zslack/shared/db";
// import {
// NodePgConnection,
// type NodePgZeroTransaction,
// } from "drizzle-zero/node-postgres";
import crypto from "node:crypto";
// import { db } from "./db";
import { getHono } from "./hono";
// type ServerTx = ServerTransaction<Schema, NodePgZeroTransaction<typeof db>>;
// const processor = new PushProcessor(
// new ZQLDatabase(new NodePgConnection(db), schema),
// );
//
// const createMutators = (authData: AuthData | null) => {
// const mutators = createMutatorsShared(authData);
//
// return {
// ...mutators,
// message: {
// ...mutators.message,
// async sendMessage(tx: ServerTx, params) {
// isLoggedIn(authData);
//
// await mutators.message.sendMessage(tx, params);
//
// // we can use the db tx to insert server-only data, like audit logs
// await tx.dbTransaction.wrappedTransaction.insert(auditLogs).values({
// id: crypto.randomUUID(),
// userId: authData.user.id,
// action: "sendMessage",
// });
// },
// },
// } as const satisfies Mutators;
// };
const zero = getHono()
// .post("/mutate", async (c) => {
// // get the auth data from betterauth
// const authData = c.get("auth");
//
// const result = await processor.process(createMutators(authData), c.req.raw);
//
// return c.json(result);
// })
.post("/get-queries", async (c) => {
// get the auth data from betterauth
const authData = c.get("auth");
const result = await handleGetQueriesRequest(
(name, args) => ({ query: getQuery(authData, name, args) }),
schema,
c.req.raw,
);
return c.json(result);
});
const validatedQueries = Object.fromEntries(
Object.values(queries).map((q) => [q.queryName, withValidation(q)]),
);
function getQuery(
authData: AuthData | null,
name: string,
args: readonly ReadonlyJSONValue[],
) {
if (name in validatedQueries) {
const q = validatedQueries[name];
return q(authData, ...args);
}
throw new Error(`Unknown query: ${name}`);
}
export { zero };

14
api/tsconfig.json Normal file
View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx",
"outDir": "./dist"
},
"exclude": ["node_modules"]
}