Compare commits

...

14 Commits

Author SHA1 Message Date
Max Koon
7adb76f752 feat: upgrade drizzle-zero 2025-10-24 09:20:37 -04:00
Max Koon
018dab38c0 feat: upgrade zero 2025-10-23 18:14:23 -04:00
Max Koon
b428ce172d feat: replace IP with tailscale url 2025-10-23 16:19:00 -04:00
Max Koon
92a2057179 chore: better env management and readme 2025-10-22 08:22:48 -04:00
Max Koon
0f958feb8d fix: settings screen 2025-10-20 21:49:08 -04:00
Max Koon
acfe62eb37 fix: cookie stuff 2025-10-20 16:53:01 -04:00
Max Koon
213c819cfd debug: add print 2025-10-20 16:44:39 -04:00
Max Koon
d8e5e8e522 feat: add prod url 2025-10-20 16:27:34 -04:00
Max Koon
9aad5a7e4e feat: add money url 2025-10-20 16:11:22 -04:00
Max Koon
3ea28db9ac feat: cors 2025-10-20 15:33:26 -04:00
Max Koon
1b841c7d32 feat: enable cors 2025-10-20 15:21:54 -04:00
Max Koon
b0ebc98d42 feat: disable cors 2025-10-20 15:19:59 -04:00
Max Koon
38739a3a0c feat: add tsx to api 2025-10-20 10:39:37 -04:00
Max Koon
763322a492 feat: dynamic port 2025-10-20 10:39:20 -04:00
23 changed files with 2203 additions and 883 deletions

View File

@@ -12,3 +12,9 @@ ZERO_GET_QUERIES_FORWARD_COOKIES="true"
ZERO_MUTATE_URL="http://localhost:3000/api/zero/mutate" ZERO_MUTATE_URL="http://localhost:3000/api/zero/mutate"
ZERO_MUTATE_FORWARD_COOKIES="true" ZERO_MUTATE_FORWARD_COOKIES="true"
PLAID_CLIENT_ID=
PLAID_SECRET=
PLAID_ENV=sandbox
EXPO_PUBLIC_TAILSCALE_MACHINE=laptop

4
.gitignore vendored
View File

@@ -31,7 +31,8 @@ yarn-error.*
*.pem *.pem
# local env files # local env files
.env .env*
!.env.example
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
@@ -45,4 +46,3 @@ app-example
.direnv/ .direnv/
.db/ .db/
.logs .logs
.env

View File

@@ -1,50 +1,20 @@
# Welcome to your Expo app đź‘‹ # Money
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). Personal finance application by [Max Koon](https://max.koon.us). Writen with Typescript, Expo/React Native, Zero Sync Engine, Better Auth, and Drizzle.
## Get started ## Development
1. Install dependencies ```sh
git clone https://git.koon.us/max/money.git
```bash direnv allow
npm install cp .env.example .env.dev
ln -s .env.dev .env
vim .env.dev # Update with Plaid credentials
pnpm install
pnpm dev
``` ```
2. Start the app ## Deployment
```bash An example deployment of this application can be found [here](https://git.koon.us/max/os/src/branch/main/host/ark/service/money.nix).
npx expo start
```
In the output, you'll find options to open the app in a
- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
## Get a fresh project
When you're ready, run:
```bash
npm run reset-project
```
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
## Learn more
To learn more about developing your project with Expo, look at the following resources:
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
## Join the community
Join our community of developers creating universal apps.
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.

View File

@@ -11,7 +11,8 @@
"@money/shared": "link:../shared", "@money/shared": "link:../shared",
"better-auth": "^1.3.27", "better-auth": "^1.3.27",
"hono": "^4.9.12", "hono": "^4.9.12",
"plaid": "^39.0.0" "plaid": "^39.0.0",
"tsx": "^4.20.6"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.7.2" "@types/node": "^24.7.2"

View File

@@ -4,6 +4,7 @@ import { genericOAuth } from "better-auth/plugins";
import { expo } from "@better-auth/expo"; import { expo } from "@better-auth/expo";
import { drizzleSchema } from "@money/shared/db"; import { drizzleSchema } from "@money/shared/db";
import { db } from "./db"; import { db } from "./db";
import { BASE_URL, HOST } from "@money/shared";
export const auth = betterAuth({ export const auth = betterAuth({
database: drizzleAdapter(db, { database: drizzleAdapter(db, {
@@ -11,7 +12,19 @@ export const auth = betterAuth({
provider: "pg", provider: "pg",
usePlural: true, usePlural: true,
}), }),
trustedOrigins: ["money://", "http://localhost:8081"], trustedOrigins: [
"http://localhost:8081",
`exp://${HOST}:8081`,
`${BASE_URL}:8081`,
"https://money.koon.us",
"money://",
],
advanced: {
crossSubDomainCookies: {
enabled: process.env.NODE_ENV == 'production',
domain: "koon.us",
},
},
plugins: [ plugins: [
expo(), expo(),
genericOAuth({ genericOAuth({

View File

@@ -1,5 +1,6 @@
import { serve } from "@hono/node-server"; import { serve } from "@hono/node-server";
import { authDataSchema } from "@money/shared/auth"; import { authDataSchema } from "@money/shared/auth";
import { BASE_URL } from "@money/shared";
import { cors } from "hono/cors"; import { cors } from "hono/cors";
import { auth } from "./auth"; import { auth } from "./auth";
import { getHono } from "./hono"; import { getHono } from "./hono";
@@ -10,7 +11,7 @@ const app = getHono();
app.use( app.use(
"/api/*", "/api/*",
cors({ cors({
origin: (origin) => origin ?? "", origin: ['https://money.koon.us', `${BASE_URL}:8081`],
allowMethods: ["POST", "GET", "OPTIONS"], allowMethods: ["POST", "GET", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization"], allowHeaders: ["Content-Type", "Authorization"],
credentials: true, credentials: true,
@@ -47,7 +48,7 @@ app.get("/", (c) => c.text("OK"));
serve( serve(
{ {
fetch: app.fetch, fetch: app.fetch,
port: 3000, port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
}, },
(info) => { (info) => {
console.log(`Server is running on ${info.address}:${info.port}`); console.log(`Server is running on ${info.address}:${info.port}`);

View File

@@ -24,11 +24,11 @@ import { Configuration, CountryCode, PlaidApi, PlaidEnvironments, Products } fro
import { randomUUID } from "crypto"; import { randomUUID } from "crypto";
import { db } from "./db"; import { db } from "./db";
import { balance, plaidAccessTokens, plaidLink, transaction } from "@money/shared/db"; import { balance, plaidAccessTokens, plaidLink, transaction } from "@money/shared/db";
import { asc, desc, eq, inArray, sql, type InferInsertModel } from "drizzle-orm"; import { eq, inArray, sql, type InferInsertModel } from "drizzle-orm";
const configuration = new Configuration({ const configuration = new Configuration({
basePath: PlaidEnvironments.production, basePath: process.env.PLAID_ENV == 'production' ? PlaidEnvironments.production : PlaidEnvironments.sandbox,
baseOptions: { baseOptions: {
headers: { headers: {
'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID, 'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID,
@@ -116,7 +116,7 @@ const createMutators = (authData: AuthData | null) => {
const { data } = await plaidClient.transactionsGet({ const { data } = await plaidClient.transactionsGet({
access_token: account.token, access_token: account.token,
start_date: "2025-10-01", start_date: "2025-10-01",
end_date: "2025-10-15", end_date: new Date().toISOString().split("T")[0],
}); });
const transactions = data.transactions.map(tx => ({ const transactions = data.transactions.map(tx => ({

View File

@@ -7,7 +7,7 @@ import { useMemo } from 'react';
import { authDataSchema } from '@/shared/src/auth'; import { authDataSchema } from '@/shared/src/auth';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import type { ZeroOptions } from '@rocicorp/zero'; import type { ZeroOptions } from '@rocicorp/zero';
import { schema, type Schema, createMutators, type Mutators } from '@/shared/src'; import { schema, type Schema, createMutators, type Mutators, BASE_URL } from '@/shared/src';
import { expoSQLiteStoreProvider } from "@rocicorp/zero/react-native"; import { expoSQLiteStoreProvider } from "@rocicorp/zero/react-native";
export const unstable_settings = { export const unstable_settings = {
@@ -32,7 +32,7 @@ export default function RootLayout() {
return { return {
storageKey: 'money', storageKey: 'money',
kvStore, kvStore,
server: process.env.NODE_ENV == 'production' ? 'https://zero.koon.us' : 'http://localhost:4848', server: process.env.NODE_ENV == 'production' ? 'https://zero.koon.us' : `${BASE_URL}:4848`,
userID: authData?.user.id ?? "anon", userID: authData?.user.id ?? "anon",
schema, schema,
mutators: createMutators(authData), mutators: createMutators(authData),

View File

@@ -1,11 +1,12 @@
import { Button, View } from "react-native"; import { Button, View } from "react-native";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
import { BASE_URL } from "@money/shared";
export default function Auth() { export default function Auth() {
const onLogin = () => { const onLogin = () => {
authClient.signIn.oauth2({ authClient.signIn.oauth2({
providerId: "koon-family", providerId: "koon-family",
callbackURL: "http://localhost:8081" callbackURL: process.env.NODE_ENV == 'production' ? 'https://money.koon.us' : `${BASE_URL}:8081`,
}); });
}; };

View File

@@ -1,83 +1,35 @@
import { authClient } from '@/lib/auth-client'; import { authClient } from '@/lib/auth-client';
import { Pressable, ScrollView, Text, View } from 'react-native'; import { RefreshControl, ScrollView, StatusBar, Text, View } from 'react-native';
import { useQuery, useZero } from "@rocicorp/zero/react"; import { useQuery, useZero } from "@rocicorp/zero/react";
import { queries, type Mutators, type Schema } from '@money/shared'; import { queries, type Mutators, type Schema } from '@money/shared';
import { useEffect, useState } from 'react'; import { useState } from 'react';
export default function HomeScreen() { export default function HomeScreen() {
const { data: session } = authClient.useSession(); const { data: session } = authClient.useSession();
const z = useZero<Schema, Mutators>();
const [transactions] = useQuery(queries.allTransactions(session));
const [balances] = useQuery(queries.getBalances(session)); const [balances] = useQuery(queries.getBalances(session));
const [refreshing, setRefreshing] = useState(false);
const [idx, setIdx] = useState(0); const onRefresh = async () => {
const [accountIdx, setAccountIdx] = useState(0); setRefreshing(true);
// simulate async work
const account = balances.at(accountIdx)!; await new Promise((resolve) => setTimeout(resolve, 1000));
setRefreshing(false);
const filteredTransactions = transactions
.filter(t => t.account_id == account.plaid_id)
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "j") {
setIdx((prevIdx) => {
if (prevIdx + 1 == filteredTransactions.length) return prevIdx;
return prevIdx + 1
});
} else if (event.key === "k") {
setIdx((prevIdx) => prevIdx == 0 ? 0 : prevIdx - 1);
} else if (event.key == 'g') {
setIdx(0);
} else if (event.key == "G") {
setIdx(transactions.length - 1);
} else if (event.key == 'R') {
z.mutate.link.updateTransactions();
z.mutate.link.updateBalences();
} else if (event.key == 'h') {
setAccountIdx((prevIdx) => prevIdx == 0 ? 0 : prevIdx - 1);
} else if (event.key == 'l') {
setAccountIdx((prevIdx) => {
if (prevIdx + 1 == balances.length) return prevIdx;
return prevIdx + 1
});
}
}; };
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [filteredTransactions, balances]);
function lpad(n: number): string {
const LEN = 9;
const nstr = n.toFixed(2).toLocaleString();
return Array.from({ length: LEN - nstr.length }).join(" ") + nstr;
}
return ( return (
<View> <>
<View style={{ flexDirection: "row" }}> <StatusBar barStyle="dark-content" />
<View style={{ backgroundColor: '' }}> <ScrollView contentContainerStyle={{ paddingTop: StatusBar.currentHeight, flexGrow: 1 }} refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />} style={{ paddingHorizontal: 10 }}>
{balances.map((bal, i) => <View key={bal.id} style={{ backgroundColor: i == accountIdx ? 'black' : undefined}}> {balances.map(balance => <Balance key={balance.id} balance={balance} />)}
<Text style={{ fontFamily: 'mono', color: i == accountIdx ? 'white' : undefined }}>{bal.name}: {bal.current} ({bal.avaliable})</Text>
</View>)}
</View>
<View>
{filteredTransactions.map((t, i) => <Pressable onHoverIn={() => {
setIdx(i);
}} style={{ backgroundColor: i == idx ? 'black' : undefined, cursor: 'default' as 'auto' }} key={t.id}>
<Text style={{ fontFamily: 'mono', color: i == idx ? 'white' : undefined }}>{new Date(t.datetime!).toDateString()} <Text style={{ color: t.amount > 0 ? 'red' : 'green' }}>{lpad(t.amount)}</Text> {t.name.substring(0, 50)}</Text>
</Pressable>)}
</View>
<ScrollView>
<Text style={{ fontFamily: 'mono' }}>{JSON.stringify(JSON.parse(filteredTransactions.at(idx)?.json || "null"), null, 4)}</Text>
</ScrollView> </ScrollView>
</View> </>
</View>
); );
} }
function Balance({ balance }: { balance: { name: string, current: number, avaliable: number } }) {
return <View style={{ backgroundColor: "#eee", borderColor: "#ddd", borderWidth: 1, marginBottom: 10, borderRadius: 10 }}>
<Text style={{ fontSize: 15, textAlign: "center" }}>{balance.name}</Text>
<Text style={{ fontSize: 30, textAlign: "center" }}>{balance.current}</Text>
</View>
}

99
app/index.web.tsx Normal file
View File

@@ -0,0 +1,99 @@
import { authClient } from '@/lib/auth-client';
import { Button, Image, Platform, Pressable, ScrollView, Text, View } from 'react-native';
import { useQuery, useZero } from "@rocicorp/zero/react";
import { queries, type Mutators, type Schema } from '@money/shared';
import { useEffect, useState } from 'react';
import { Link } from 'expo-router';
export default function HomeScreen() {
const { data: session } = authClient.useSession();
const z = useZero<Schema, Mutators>();
const [transactions] = useQuery(queries.allTransactions(session));
const [balances] = useQuery(queries.getBalances(session));
const [idx, setIdx] = useState(0);
const [accountIdx, setAccountIdx] = useState(0);
const account = balances.at(accountIdx)!;
const filteredTransactions = transactions
.filter(t => t.account_id == account.plaid_id)
useEffect(() => {
if (Platform.OS != 'web') return;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "j") {
setIdx((prevIdx) => {
if (prevIdx + 1 == filteredTransactions.length) return prevIdx;
return prevIdx + 1
});
} else if (event.key === "k") {
setIdx((prevIdx) => prevIdx == 0 ? 0 : prevIdx - 1);
} else if (event.key == 'g') {
setIdx(0);
} else if (event.key == "G") {
setIdx(transactions.length - 1);
} else if (event.key == 'R') {
z.mutate.link.updateTransactions();
z.mutate.link.updateBalences();
} else if (event.key == 'h') {
setAccountIdx((prevIdx) => prevIdx == 0 ? 0 : prevIdx - 1);
} else if (event.key == 'l') {
setAccountIdx((prevIdx) => {
if (prevIdx + 1 == balances.length) return prevIdx;
return prevIdx + 1
});
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [filteredTransactions, balances]);
function lpad(n: number): string {
const LEN = 9;
const nstr = n.toFixed(2).toLocaleString();
return Array.from({ length: LEN - nstr.length }).join(" ") + nstr;
}
function uuu(t: typeof filteredTransactions[number]): string | undefined {
if (!t.json) return;
const j = JSON.parse(t.json);
return j.counterparties.filter((c: any) => !!c.logo_url).at(0)?.logo_url || j.personal_finance_category_icon_url;
}
return (
<View>
<Link prefetch href="/settings">
<Button title="Settings" />
</Link>
<View style={{ flexDirection: "row" }}>
<View style={{ backgroundColor: '' }}>
{balances.map((bal, i) => <View key={bal.id} style={{ backgroundColor: i == accountIdx ? 'black' : undefined}}>
<Text style={{ fontFamily: 'mono', color: i == accountIdx ? 'white' : undefined }}>{bal.name}: {bal.current} ({bal.avaliable})</Text>
</View>)}
</View>
<View>
{filteredTransactions.map((t, i) => <Pressable onHoverIn={() => {
setIdx(i);
}} style={{ backgroundColor: i == idx ? 'black' : undefined, cursor: 'default' as 'auto' }} key={t.id}>
<Text style={{ fontFamily: 'mono', color: i == idx ? 'white' : undefined }}>
{new Date(t.datetime!).toDateString()}
<Text style={{ color: t.amount > 0 ? 'red' : 'green' }}> {lpad(t.amount)}</Text>
<Image style={{ width: 15, height: 15, marginHorizontal: 10 }} source={{ uri: uuu(t) || "" }} />
{t.name.substring(0, 50)}
</Text>
</Pressable>)}
</View>
<ScrollView>
<Text style={{ fontFamily: 'mono' }}>{JSON.stringify(JSON.parse(filteredTransactions.at(idx)?.json || "null"), null, 4)}</Text>
</ScrollView>
</View>
</View>
);
}

View File

@@ -2,9 +2,10 @@ import { createAuthClient } from "better-auth/react";
import { genericOAuthClient } from "better-auth/client/plugins"; import { genericOAuthClient } from "better-auth/client/plugins";
import { expoClient } from "@better-auth/expo/client"; import { expoClient } from "@better-auth/expo/client";
import * as SecureStore from "expo-secure-store"; import * as SecureStore from "expo-secure-store";
import { BASE_URL } from "@money/shared";
export const authClient = createAuthClient({ export const authClient = createAuthClient({
baseURL: process.env.NODE_ENV == 'production' ? 'https://money-api.koon.us' : "http://localhost:3000", baseURL: process.env.NODE_ENV == 'production' ? 'https://money-api.koon.us' : `${BASE_URL}:3000`,
plugins: [ plugins: [
expoClient({ expoClient({
scheme: "money", scheme: "money",

View File

@@ -21,7 +21,7 @@
"@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3", "@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8", "@react-navigation/native": "^7.1.8",
"@rocicorp/zero": "^0.23.2025090100", "@rocicorp/zero": "^0.24.2025101500",
"better-auth": "^1.3.27", "better-auth": "^1.3.27",
"drizzle-orm": "^0.44.6", "drizzle-orm": "^0.44.6",
"expo": "~54.0.13", "expo": "~54.0.13",

2587
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,10 +25,14 @@ processes:
initial_delay_seconds: 2 initial_delay_seconds: 2
period_seconds: 1 period_seconds: 1
tailscale_machine_name:
command: "pnpm tsx ./scripts/set-machine-name.ts"
expo: expo:
command: "pnpm start" command: "pnpm start"
environment: depends_on:
- "DATABASE_URL=postgresql://postgres@localhost:5432/money" tailscale_machine_name:
condition: process_completed_successfully
api: api:
command: "pnpm run dev" command: "pnpm run dev"

36
scripts/set-machine-name.ts Executable file
View File

@@ -0,0 +1,36 @@
import { execSync } from "child_process";
import fs from "fs";
const ENV_FILE = ".env.dev";
const VARIABLE_NAME = "EXPO_PUBLIC_TAILSCALE_MACHINE";
try {
const json = execSync("tailscale status --self --json", { encoding: "utf8" });
const data = JSON.parse(json);
const machine = data?.Self?.DNSName.split(".")[0];
if (!machine) {
console.error("Error: could not retrieve Tailscale machine name");
process.exit(1);
}
let content = "";
if (fs.existsSync(ENV_FILE)) {
content = fs.readFileSync(ENV_FILE, "utf8");
}
const pattern = new RegExp(`^${VARIABLE_NAME}=.*`, "m");
if (pattern.test(content)) {
content = content.replace(pattern, `${VARIABLE_NAME}=${machine}`);
} else {
if (content && !content.endsWith("\n")) content += "\n";
content += `${VARIABLE_NAME}=${machine}\n`;
}
fs.writeFileSync(ENV_FILE, content, "utf8");
console.log(`Updated ${ENV_FILE} with ${VARIABLE_NAME}=${machine}`);
} catch (err) {
console.error("Failed to update .env.dev:", err);
process.exit(1);
}

View File

@@ -9,10 +9,10 @@
"./db": "./src/db/index.ts" "./db": "./src/db/index.ts"
}, },
"dependencies": { "dependencies": {
"drizzle-zero": "^0.14.3" "drizzle-zero": "^0.15.1"
}, },
"scripts": { "scripts": {
"generate:zero": "drizzle-zero generate -s ./src/db/schema/public.ts -o ./src/zero-schema.gen.ts -f && sed -i 's/enableLegacyQueries: true,/enableLegacyQueries: false,/g' src/zero-schema.gen.ts && sed -i 's/enableLegacyMutators: true,/enableLegacyMutators: false,/g' src/zero-schema.gen.ts", "generate:zero": "drizzle-zero generate -s ./src/db/schema/public.ts -o ./src/zero-schema.gen.ts -f --disable-legacy-queries --disable-legacy-mutators",
"db:migrate": "drizzle-kit push" "db:migrate": "drizzle-kit push"
} }
} }

3
shared/src/const.ts Normal file
View File

@@ -0,0 +1,3 @@
export const HOST = process.env.EXPO_PUBLIC_TAILSCALE_MACHINE || "localhost";
export const BASE_URL = `http://${HOST}`;

View File

@@ -2,3 +2,4 @@ export * from "./queries";
export * from "./mutators"; export * from "./mutators";
export * from "./zero-schema.gen"; export * from "./zero-schema.gen";
export * from "./zql"; export * from "./zql";
export * from "./const";

View File

@@ -1,23 +1,22 @@
import { syncedQueryWithContext } from "@rocicorp/zero"; import { syncedQueryWithContext } from "@rocicorp/zero";
import { z } from "zod"; import { z } from "zod";
import { builder } from "@money/shared"; import { builder } from ".";
import { type AuthData } from "./auth"; import { type AuthData } from "./auth";
import { isLoggedIn } from "./zql"; import { isLoggedIn } from "./zql";
export const queries = { export const queries = {
me: syncedQueryWithContext('me', z.tuple([]), (authData: AuthData | null) => {
isLoggedIn(authData);
return builder.users
.where('id', '=', authData.user.id)
.one();
}),
allTransactions: syncedQueryWithContext('allTransactions', z.tuple([]), (authData: AuthData | null) => { allTransactions: syncedQueryWithContext('allTransactions', z.tuple([]), (authData: AuthData | null) => {
isLoggedIn(authData); isLoggedIn(authData);
return builder.transaction return builder.transaction
.where('user_id', '=', authData.user.id) .where('user_id', '=', authData.user.id)
.orderBy('datetime', 'desc') .orderBy('datetime', 'desc')
.limit(50) .limit(50)
}
),
me: syncedQueryWithContext('me', z.tuple([]), (authData: AuthData | null) => {
isLoggedIn(authData);
return builder.users
.where('id', '=', authData.user.id)
.one();
}), }),
getPlaidLink: syncedQueryWithContext('getPlaidLink', z.tuple([]), (authData: AuthData | null) => { getPlaidLink: syncedQueryWithContext('getPlaidLink', z.tuple([]), (authData: AuthData | null) => {
isLoggedIn(authData); isLoggedIn(authData);
@@ -29,6 +28,7 @@ export const queries = {
getBalances: syncedQueryWithContext('getBalances', z.tuple([]), (authData: AuthData | null) => { getBalances: syncedQueryWithContext('getBalances', z.tuple([]), (authData: AuthData | null) => {
isLoggedIn(authData); isLoggedIn(authData);
return builder.balance return builder.balance
.where('user_id', '=', authData.user.id); .where('user_id', '=', authData.user.id)
.orderBy('name', 'asc');
}) })
}; };

View File

@@ -10,11 +10,9 @@
import type { Row } from "@rocicorp/zero"; import type { Row } from "@rocicorp/zero";
import { createBuilder } from "@rocicorp/zero"; import { createBuilder } from "@rocicorp/zero";
import type { DrizzleToZeroSchema, ZeroCustomType } from "drizzle-zero"; import type { CustomType } from "drizzle-zero";
import type * as drizzleSchema from "./db/schema/public"; import type * as drizzleSchema from "./db/schema/public";
type ZeroSchema = DrizzleToZeroSchema<typeof drizzleSchema>;
/** /**
* The Zero schema object. * The Zero schema object.
* This type is auto-generated from your Drizzle schema definition. * This type is auto-generated from your Drizzle schema definition.
@@ -27,8 +25,8 @@ export const schema = {
id: { id: {
type: "string", type: "string",
optional: false, optional: false,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"balance", "balance",
"id" "id"
>, >,
@@ -36,8 +34,8 @@ export const schema = {
user_id: { user_id: {
type: "string", type: "string",
optional: false, optional: false,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"balance", "balance",
"user_id" "user_id"
>, >,
@@ -46,8 +44,8 @@ export const schema = {
plaid_id: { plaid_id: {
type: "string", type: "string",
optional: false, optional: false,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"balance", "balance",
"plaid_id" "plaid_id"
>, >,
@@ -56,8 +54,8 @@ export const schema = {
avaliable: { avaliable: {
type: "number", type: "number",
optional: false, optional: false,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"balance", "balance",
"avaliable" "avaliable"
>, >,
@@ -65,8 +63,8 @@ export const schema = {
current: { current: {
type: "number", type: "number",
optional: false, optional: false,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"balance", "balance",
"current" "current"
>, >,
@@ -74,8 +72,8 @@ export const schema = {
name: { name: {
type: "string", type: "string",
optional: false, optional: false,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"balance", "balance",
"name" "name"
>, >,
@@ -83,8 +81,8 @@ export const schema = {
createdAt: { createdAt: {
type: "number", type: "number",
optional: true, optional: true,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"balance", "balance",
"createdAt" "createdAt"
>, >,
@@ -93,8 +91,8 @@ export const schema = {
updatedAt: { updatedAt: {
type: "number", type: "number",
optional: true, optional: true,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"balance", "balance",
"updatedAt" "updatedAt"
>, >,
@@ -109,8 +107,8 @@ export const schema = {
id: { id: {
type: "string", type: "string",
optional: false, optional: false,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"plaidLink", "plaidLink",
"id" "id"
>, >,
@@ -118,8 +116,8 @@ export const schema = {
user_id: { user_id: {
type: "string", type: "string",
optional: false, optional: false,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"plaidLink", "plaidLink",
"user_id" "user_id"
>, >,
@@ -127,8 +125,8 @@ export const schema = {
link: { link: {
type: "string", type: "string",
optional: false, optional: false,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"plaidLink", "plaidLink",
"link" "link"
>, >,
@@ -136,8 +134,8 @@ export const schema = {
token: { token: {
type: "string", type: "string",
optional: false, optional: false,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"plaidLink", "plaidLink",
"token" "token"
>, >,
@@ -145,8 +143,8 @@ export const schema = {
createdAt: { createdAt: {
type: "number", type: "number",
optional: true, optional: true,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"plaidLink", "plaidLink",
"createdAt" "createdAt"
>, >,
@@ -161,8 +159,8 @@ export const schema = {
id: { id: {
type: "string", type: "string",
optional: false, optional: false,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"transaction", "transaction",
"id" "id"
>, >,
@@ -170,8 +168,8 @@ export const schema = {
user_id: { user_id: {
type: "string", type: "string",
optional: false, optional: false,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"transaction", "transaction",
"user_id" "user_id"
>, >,
@@ -179,8 +177,8 @@ export const schema = {
plaid_id: { plaid_id: {
type: "string", type: "string",
optional: false, optional: false,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"transaction", "transaction",
"plaid_id" "plaid_id"
>, >,
@@ -188,8 +186,8 @@ export const schema = {
account_id: { account_id: {
type: "string", type: "string",
optional: false, optional: false,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"transaction", "transaction",
"account_id" "account_id"
>, >,
@@ -197,8 +195,8 @@ export const schema = {
name: { name: {
type: "string", type: "string",
optional: false, optional: false,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"transaction", "transaction",
"name" "name"
>, >,
@@ -206,8 +204,8 @@ export const schema = {
amount: { amount: {
type: "number", type: "number",
optional: false, optional: false,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"transaction", "transaction",
"amount" "amount"
>, >,
@@ -215,8 +213,8 @@ export const schema = {
datetime: { datetime: {
type: "number", type: "number",
optional: true, optional: true,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"transaction", "transaction",
"datetime" "datetime"
>, >,
@@ -224,8 +222,8 @@ export const schema = {
authorized_datetime: { authorized_datetime: {
type: "number", type: "number",
optional: true, optional: true,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"transaction", "transaction",
"authorized_datetime" "authorized_datetime"
>, >,
@@ -233,8 +231,8 @@ export const schema = {
json: { json: {
type: "string", type: "string",
optional: true, optional: true,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"transaction", "transaction",
"json" "json"
>, >,
@@ -242,8 +240,8 @@ export const schema = {
createdAt: { createdAt: {
type: "number", type: "number",
optional: true, optional: true,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"transaction", "transaction",
"createdAt" "createdAt"
>, >,
@@ -252,8 +250,8 @@ export const schema = {
updatedAt: { updatedAt: {
type: "number", type: "number",
optional: true, optional: true,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"transaction", "transaction",
"updatedAt" "updatedAt"
>, >,
@@ -268,8 +266,8 @@ export const schema = {
id: { id: {
type: "string", type: "string",
optional: false, optional: false,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"users", "users",
"id" "id"
>, >,
@@ -277,8 +275,8 @@ export const schema = {
name: { name: {
type: "string", type: "string",
optional: true, optional: true,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"users", "users",
"name" "name"
>, >,
@@ -286,8 +284,8 @@ export const schema = {
email: { email: {
type: "string", type: "string",
optional: false, optional: false,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"users", "users",
"email" "email"
>, >,
@@ -295,8 +293,8 @@ export const schema = {
emailVerified: { emailVerified: {
type: "boolean", type: "boolean",
optional: true, optional: true,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"users", "users",
"emailVerified" "emailVerified"
>, >,
@@ -305,8 +303,8 @@ export const schema = {
image: { image: {
type: "string", type: "string",
optional: true, optional: true,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"users", "users",
"image" "image"
>, >,
@@ -314,8 +312,8 @@ export const schema = {
createdAt: { createdAt: {
type: "number", type: "number",
optional: true, optional: true,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"users", "users",
"createdAt" "createdAt"
>, >,
@@ -324,8 +322,8 @@ export const schema = {
updatedAt: { updatedAt: {
type: "number", type: "number",
optional: true, optional: true,
customType: null as unknown as ZeroCustomType< customType: null as unknown as CustomType<
ZeroSchema, typeof drizzleSchema,
"users", "users",
"updatedAt" "updatedAt"
>, >,

View File

@@ -3,6 +3,7 @@ import type { AuthData } from "./auth";
export function isLoggedIn( export function isLoggedIn(
authData: AuthData | null, authData: AuthData | null,
): asserts authData is AuthData { ): asserts authData is AuthData {
console.log("AUTHDATA", authData);
if (!authData?.user.id) { if (!authData?.user.id) {
throw new Error("User is not logged in"); throw new Error("User is not logged in");
} }

View File

@@ -3,7 +3,7 @@
"target": "ES2022", "target": "ES2022",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Bundler", "moduleResolution": "Bundler",
"declaration": true, "declaration": false,
"outDir": "./dist", "outDir": "./dist",
"strict": true, "strict": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,