feat: query plaid transactions
This commit is contained in:
@@ -9,7 +9,8 @@
|
|||||||
"@hono/node-server": "^1.19.5",
|
"@hono/node-server": "^1.19.5",
|
||||||
"@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"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.7.2"
|
"@types/node": "^24.7.2"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
type ReadonlyJSONValue,
|
type ReadonlyJSONValue,
|
||||||
|
type Transaction,
|
||||||
withValidation,
|
withValidation,
|
||||||
} from "@rocicorp/zero";
|
} from "@rocicorp/zero";
|
||||||
import {
|
import {
|
||||||
@@ -11,13 +12,31 @@ import { PostgresJSConnection } from '@rocicorp/zero/pg';
|
|||||||
import postgres from 'postgres';
|
import postgres from 'postgres';
|
||||||
import {
|
import {
|
||||||
createMutators as createMutatorsShared,
|
createMutators as createMutatorsShared,
|
||||||
|
isLoggedIn,
|
||||||
queries,
|
queries,
|
||||||
schema,
|
schema,
|
||||||
type Mutators,
|
type Mutators,
|
||||||
|
type Schema,
|
||||||
} from "@money/shared";
|
} from "@money/shared";
|
||||||
import type { AuthData } from "@money/shared/auth";
|
import type { AuthData } from "@money/shared/auth";
|
||||||
import { getHono } from "./hono";
|
import { getHono } from "./hono";
|
||||||
|
import { Configuration, CountryCode, PlaidApi, PlaidEnvironments, Products } from "plaid";
|
||||||
|
import { randomUUID } from "crypto";
|
||||||
import { db } from "./db";
|
import { db } from "./db";
|
||||||
|
import { plaidAccessTokens, plaidLink, transaction } from "@money/shared/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
|
||||||
|
const configuration = new Configuration({
|
||||||
|
basePath: PlaidEnvironments.production,
|
||||||
|
baseOptions: {
|
||||||
|
headers: {
|
||||||
|
'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID,
|
||||||
|
'PLAID-SECRET': process.env.PLAID_SECRET,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const plaidClient = new PlaidApi(configuration);
|
||||||
|
|
||||||
|
|
||||||
const processor = new PushProcessor(
|
const processor = new PushProcessor(
|
||||||
@@ -27,6 +46,8 @@ const processor = new PushProcessor(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
type Tx = Transaction<Schema>;
|
||||||
|
|
||||||
const createMutators = (authData: AuthData | null) => {
|
const createMutators = (authData: AuthData | null) => {
|
||||||
const mutators = createMutatorsShared(authData);
|
const mutators = createMutatorsShared(authData);
|
||||||
return {
|
return {
|
||||||
@@ -34,7 +55,73 @@ const createMutators = (authData: AuthData | null) => {
|
|||||||
link: {
|
link: {
|
||||||
...mutators.link,
|
...mutators.link,
|
||||||
async create() {
|
async create() {
|
||||||
console.log("Here is my function running on the server!!!");
|
isLoggedIn(authData);
|
||||||
|
console.log("Creating Link token");
|
||||||
|
const r = await plaidClient.linkTokenCreate({
|
||||||
|
user: {
|
||||||
|
client_user_id: authData.user.id,
|
||||||
|
},
|
||||||
|
client_name: "Koon Money",
|
||||||
|
language: "en",
|
||||||
|
products: [Products.Transactions],
|
||||||
|
country_codes: [CountryCode.Us],
|
||||||
|
hosted_link: {}
|
||||||
|
});
|
||||||
|
console.log("Result", r);
|
||||||
|
const { link_token, hosted_link_url } = r.data;
|
||||||
|
|
||||||
|
if (!hosted_link_url) throw Error("No link in response");
|
||||||
|
|
||||||
|
await db.insert(plaidLink).values({
|
||||||
|
id: randomUUID() as string,
|
||||||
|
user_id: authData.user.id,
|
||||||
|
link: hosted_link_url,
|
||||||
|
token: link_token,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async get(_, { link_token }) {
|
||||||
|
isLoggedIn(authData);
|
||||||
|
|
||||||
|
const linkResp = await plaidClient.linkTokenGet({
|
||||||
|
link_token,
|
||||||
|
});
|
||||||
|
if (!linkResp) throw Error("No link respo");
|
||||||
|
console.log(JSON.stringify(linkResp.data, null, 4));
|
||||||
|
const publicToken = linkResp.data.link_sessions?.at(0)?.results?.item_add_results.at(0)?.public_token;
|
||||||
|
|
||||||
|
if (!publicToken) throw Error("No public token");
|
||||||
|
const { data } = await plaidClient.itemPublicTokenExchange({
|
||||||
|
public_token: publicToken,
|
||||||
|
})
|
||||||
|
|
||||||
|
await db.insert(plaidAccessTokens).values({
|
||||||
|
id: randomUUID(),
|
||||||
|
userId: authData.user.id,
|
||||||
|
token: data.access_token,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async updateTransactions() {
|
||||||
|
isLoggedIn(authData);
|
||||||
|
const accessToken = await db.query.plaidAccessTokens.findFirst({
|
||||||
|
where: eq(plaidAccessTokens.userId, authData.user.id)
|
||||||
|
});
|
||||||
|
if (!accessToken) throw Error("No plaid account");
|
||||||
|
|
||||||
|
const { data } = await plaidClient.transactionsGet({
|
||||||
|
access_token: accessToken.token,
|
||||||
|
start_date: "2025-10-01",
|
||||||
|
end_date: "2025-10-15",
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const t of data.transactions) {
|
||||||
|
await db.insert(transaction).values({
|
||||||
|
id: randomUUID(),
|
||||||
|
user_id: authData.user.id,
|
||||||
|
name: t.name,
|
||||||
|
amount: t.amount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} as const satisfies Mutators;
|
} as const satisfies Mutators;
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export default function RootLayout() {
|
|||||||
<Stack>
|
<Stack>
|
||||||
<Stack.Protected guard={!isPending && !!session}>
|
<Stack.Protected guard={!isPending && !!session}>
|
||||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||||
|
<Stack.Screen name="settings" options={{ headerShown: false }} />
|
||||||
</Stack.Protected>
|
</Stack.Protected>
|
||||||
<Stack.Protected guard={!isPending && !session}>
|
<Stack.Protected guard={!isPending && !session}>
|
||||||
<Stack.Screen name="auth" />
|
<Stack.Screen name="auth" />
|
||||||
|
|||||||
@@ -1,40 +1,47 @@
|
|||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { authClient } from '@/lib/auth-client';
|
import { authClient } from '@/lib/auth-client';
|
||||||
import { Button, Text } from 'react-native';
|
import { Button, Linking, ScrollView, 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 { randomUUID } from "expo-crypto";
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const { data: session } = authClient.useSession();
|
const { data: session } = authClient.useSession();
|
||||||
|
|
||||||
const onLogout = () => {
|
|
||||||
authClient.signOut();
|
|
||||||
}
|
|
||||||
const z = useZero<Schema, Mutators>();
|
const z = useZero<Schema, Mutators>();
|
||||||
|
const [plaidLink] = useQuery(queries.getPlaidLink(session));
|
||||||
const [transactions] = useQuery(queries.allTransactions(session));
|
const [transactions] = useQuery(queries.allTransactions(session));
|
||||||
const [user] = useQuery(queries.me(session));
|
|
||||||
|
|
||||||
const onNew = () => {
|
const [idx, setIdx] = useState(0);
|
||||||
z.mutate.transaction.create({
|
|
||||||
id: randomUUID(),
|
useEffect(() => {
|
||||||
name: "Uber",
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
amount: 100,
|
if (event.key === "j") {
|
||||||
})
|
setIdx((prevIdx) => {
|
||||||
};
|
if (prevIdx + 1 == transactions.length) return prevIdx;
|
||||||
|
return prevIdx + 1
|
||||||
|
});
|
||||||
|
} else if (event.key === "k") {
|
||||||
|
setIdx((prevIdx) => prevIdx == 0 ? 0 : prevIdx - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
|
|
||||||
|
// Cleanup listener on unmount
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [transactions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<ScrollView>
|
||||||
<Text>Hello {user?.name}</Text>
|
{plaidLink && <Button onPress={() => {
|
||||||
<Button onPress={onLogout} title="Logout" />
|
z.mutate.link.updateTransactions();
|
||||||
<Text>Transactions: {JSON.stringify(transactions, null, 4)}</Text>
|
}} title="Update transactions" />}
|
||||||
<Button onPress={onNew} title="New" />
|
{transactions.map((t, i) => <View style={{ backgroundColor: i == idx ? 'black' : undefined }} key={t.id}>
|
||||||
<Button onPress={() => {
|
<Text style={{ fontFamily: 'mono', color: i == idx ? 'white' : undefined }}>{t.name} {t.amount}</Text>
|
||||||
z.mutate.transaction.deleteAll();
|
</View>)}
|
||||||
}} title="Delete" />
|
</ScrollView>
|
||||||
<Button onPress={() => {
|
|
||||||
z.mutate.link.create();
|
|
||||||
}} title="Open link" />
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
41
app/settings.tsx
Normal file
41
app/settings.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { authClient } from '@/lib/auth-client';
|
||||||
|
import { Button, Linking, Text } from 'react-native';
|
||||||
|
import { useQuery, useZero } from "@rocicorp/zero/react";
|
||||||
|
import { queries, type Mutators, type Schema } from '@money/shared';
|
||||||
|
|
||||||
|
export default function HomeScreen() {
|
||||||
|
const { data: session } = authClient.useSession();
|
||||||
|
|
||||||
|
const onLogout = () => {
|
||||||
|
authClient.signOut();
|
||||||
|
}
|
||||||
|
const z = useZero<Schema, Mutators>();
|
||||||
|
const [user] = useQuery(queries.me(session));
|
||||||
|
const [plaidLink] = useQuery(queries.getPlaidLink(session));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView>
|
||||||
|
<Text>Hello {user?.name}</Text>
|
||||||
|
<Button onPress={onLogout} title="Logout" />
|
||||||
|
|
||||||
|
<Text>{JSON.stringify(plaidLink)}</Text>
|
||||||
|
{plaidLink ? <Button onPress={() => {
|
||||||
|
Linking.openURL(plaidLink.link);
|
||||||
|
}} title="Open Plaid" /> : <Text>No plaid link</Text>}
|
||||||
|
|
||||||
|
<Button onPress={() => {
|
||||||
|
z.mutate.link.create();
|
||||||
|
}} title="Generate Link" />
|
||||||
|
|
||||||
|
{plaidLink && <Button onPress={() => {
|
||||||
|
z.mutate.link.get({ link_token: plaidLink.token });
|
||||||
|
}} title="Check Link" />}
|
||||||
|
|
||||||
|
{plaidLink && <Button onPress={() => {
|
||||||
|
z.mutate.link.updateTransactions();
|
||||||
|
}} title="Update transactions" />}
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
71
pnpm-lock.yaml
generated
71
pnpm-lock.yaml
generated
@@ -144,6 +144,9 @@ importers:
|
|||||||
hono:
|
hono:
|
||||||
specifier: ^4.9.12
|
specifier: ^4.9.12
|
||||||
version: 4.9.12
|
version: 4.9.12
|
||||||
|
plaid:
|
||||||
|
specifier: ^39.0.0
|
||||||
|
version: 39.0.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^24.7.2
|
specifier: ^24.7.2
|
||||||
@@ -2738,6 +2741,9 @@ packages:
|
|||||||
async-limiter@1.0.1:
|
async-limiter@1.0.1:
|
||||||
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
|
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
|
||||||
|
|
||||||
|
asynckit@0.4.0:
|
||||||
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
|
|
||||||
atomic-sleep@1.0.0:
|
atomic-sleep@1.0.0:
|
||||||
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
@@ -2752,6 +2758,9 @@ packages:
|
|||||||
await-lock@2.2.2:
|
await-lock@2.2.2:
|
||||||
resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==}
|
resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==}
|
||||||
|
|
||||||
|
axios@1.12.2:
|
||||||
|
resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==}
|
||||||
|
|
||||||
babel-jest@29.7.0:
|
babel-jest@29.7.0:
|
||||||
resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==}
|
resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==}
|
||||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||||
@@ -3056,6 +3065,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
||||||
engines: {node: '>=12.5.0'}
|
engines: {node: '>=12.5.0'}
|
||||||
|
|
||||||
|
combined-stream@1.0.8:
|
||||||
|
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
command-line-args@6.0.1:
|
command-line-args@6.0.1:
|
||||||
resolution: {integrity: sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==}
|
resolution: {integrity: sha512-Jr3eByUjqyK0qd8W0SGFW1nZwqCaNCtbXjRo2cRJC1OYxWl3MZ5t1US3jq+cO4sPavqgw4l9BMGX0CBe+trepg==}
|
||||||
engines: {node: '>=12.20'}
|
engines: {node: '>=12.20'}
|
||||||
@@ -3215,6 +3228,10 @@ packages:
|
|||||||
defu@6.1.4:
|
defu@6.1.4:
|
||||||
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||||
|
|
||||||
|
delayed-stream@1.0.0:
|
||||||
|
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
depd@2.0.0:
|
depd@2.0.0:
|
||||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -3903,6 +3920,15 @@ packages:
|
|||||||
flow-enums-runtime@0.0.6:
|
flow-enums-runtime@0.0.6:
|
||||||
resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==}
|
resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==}
|
||||||
|
|
||||||
|
follow-redirects@1.15.11:
|
||||||
|
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
peerDependencies:
|
||||||
|
debug: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
debug:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fontfaceobserver@2.3.0:
|
fontfaceobserver@2.3.0:
|
||||||
resolution: {integrity: sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==}
|
resolution: {integrity: sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==}
|
||||||
|
|
||||||
@@ -3914,6 +3940,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
form-data@4.0.4:
|
||||||
|
resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
forwarded-parse@2.1.2:
|
forwarded-parse@2.1.2:
|
||||||
resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==}
|
resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==}
|
||||||
|
|
||||||
@@ -5112,6 +5142,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
plaid@39.0.0:
|
||||||
|
resolution: {integrity: sha512-7WuI97/R/xzjQV8ZSWY/rx5mFjjLDbIuGpFrOq0AolqplwT9StHe0ADa+H5rrpeOi6NNr+DdN7Ace/TACYME1w==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
plist@3.1.0:
|
plist@3.1.0:
|
||||||
resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==}
|
resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==}
|
||||||
engines: {node: '>=10.4.0'}
|
engines: {node: '>=10.4.0'}
|
||||||
@@ -5212,6 +5246,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==}
|
resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
proxy-from-env@1.1.0:
|
||||||
|
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||||
|
|
||||||
pump@3.0.3:
|
pump@3.0.3:
|
||||||
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
|
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
|
||||||
|
|
||||||
@@ -9459,6 +9496,8 @@ snapshots:
|
|||||||
|
|
||||||
async-limiter@1.0.1: {}
|
async-limiter@1.0.1: {}
|
||||||
|
|
||||||
|
asynckit@0.4.0: {}
|
||||||
|
|
||||||
atomic-sleep@1.0.0: {}
|
atomic-sleep@1.0.0: {}
|
||||||
|
|
||||||
available-typed-arrays@1.0.7:
|
available-typed-arrays@1.0.7:
|
||||||
@@ -9472,6 +9511,14 @@ snapshots:
|
|||||||
|
|
||||||
await-lock@2.2.2: {}
|
await-lock@2.2.2: {}
|
||||||
|
|
||||||
|
axios@1.12.2:
|
||||||
|
dependencies:
|
||||||
|
follow-redirects: 1.15.11
|
||||||
|
form-data: 4.0.4
|
||||||
|
proxy-from-env: 1.1.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- debug
|
||||||
|
|
||||||
babel-jest@29.7.0(@babel/core@7.28.4):
|
babel-jest@29.7.0(@babel/core@7.28.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.4
|
'@babel/core': 7.28.4
|
||||||
@@ -9841,6 +9888,10 @@ snapshots:
|
|||||||
color-convert: 2.0.1
|
color-convert: 2.0.1
|
||||||
color-string: 1.9.1
|
color-string: 1.9.1
|
||||||
|
|
||||||
|
combined-stream@1.0.8:
|
||||||
|
dependencies:
|
||||||
|
delayed-stream: 1.0.0
|
||||||
|
|
||||||
command-line-args@6.0.1:
|
command-line-args@6.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
array-back: 6.2.2
|
array-back: 6.2.2
|
||||||
@@ -9993,6 +10044,8 @@ snapshots:
|
|||||||
|
|
||||||
defu@6.1.4: {}
|
defu@6.1.4: {}
|
||||||
|
|
||||||
|
delayed-stream@1.0.0: {}
|
||||||
|
|
||||||
depd@2.0.0: {}
|
depd@2.0.0: {}
|
||||||
|
|
||||||
dequal@2.0.3: {}
|
dequal@2.0.3: {}
|
||||||
@@ -10820,6 +10873,8 @@ snapshots:
|
|||||||
|
|
||||||
flow-enums-runtime@0.0.6: {}
|
flow-enums-runtime@0.0.6: {}
|
||||||
|
|
||||||
|
follow-redirects@1.15.11: {}
|
||||||
|
|
||||||
fontfaceobserver@2.3.0: {}
|
fontfaceobserver@2.3.0: {}
|
||||||
|
|
||||||
for-each@0.3.5:
|
for-each@0.3.5:
|
||||||
@@ -10831,6 +10886,14 @@ snapshots:
|
|||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
signal-exit: 4.1.0
|
signal-exit: 4.1.0
|
||||||
|
|
||||||
|
form-data@4.0.4:
|
||||||
|
dependencies:
|
||||||
|
asynckit: 0.4.0
|
||||||
|
combined-stream: 1.0.8
|
||||||
|
es-set-tostringtag: 2.1.0
|
||||||
|
hasown: 2.0.2
|
||||||
|
mime-types: 2.1.35
|
||||||
|
|
||||||
forwarded-parse@2.1.2: {}
|
forwarded-parse@2.1.2: {}
|
||||||
|
|
||||||
freeport-async@2.0.0: {}
|
freeport-async@2.0.0: {}
|
||||||
@@ -12225,6 +12288,12 @@ snapshots:
|
|||||||
|
|
||||||
pirates@4.0.7: {}
|
pirates@4.0.7: {}
|
||||||
|
|
||||||
|
plaid@39.0.0:
|
||||||
|
dependencies:
|
||||||
|
axios: 1.12.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- debug
|
||||||
|
|
||||||
plist@3.1.0:
|
plist@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@xmldom/xmldom': 0.8.11
|
'@xmldom/xmldom': 0.8.11
|
||||||
@@ -12328,6 +12397,8 @@ snapshots:
|
|||||||
'@types/node': 24.7.2
|
'@types/node': 24.7.2
|
||||||
long: 5.3.2
|
long: 5.3.2
|
||||||
|
|
||||||
|
proxy-from-env@1.1.0: {}
|
||||||
|
|
||||||
pump@3.0.3:
|
pump@3.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
end-of-stream: 1.4.5
|
end-of-stream: 1.4.5
|
||||||
|
|||||||
@@ -92,3 +92,10 @@ export const auditLogs = pgTable("audit_log", {
|
|||||||
.references(() => users.id, { onDelete: "cascade" }),
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
action: text("action").notNull(),
|
action: text("action").notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const plaidAccessTokens = pgTable("plaidAccessToken", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
userId: text("user_id").notNull(),
|
||||||
|
token: text("token").notNull(),
|
||||||
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { integer, pgTable, text, boolean, timestamp, uniqueIndex } from "drizzle-orm/pg-core";
|
import { pgTable, text, boolean, timestamp, uniqueIndex, decimal } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
export const users = pgTable(
|
export const users = pgTable(
|
||||||
"user",
|
"user",
|
||||||
@@ -18,7 +18,14 @@ export const transaction = pgTable("transaction", {
|
|||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
user_id: text("user_id").notNull(),
|
user_id: text("user_id").notNull(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
amount: integer("amount").notNull(),
|
amount: decimal("amount").notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const plaidLink = pgTable("plaidLink", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
user_id: text("user_id").notNull(),
|
||||||
|
link: text("link").notNull(),
|
||||||
|
token: text("token").notNull(),
|
||||||
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ export function createMutators(authData: AuthData | null) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
async create() {}
|
async create() {},
|
||||||
|
async get(tx: Tx, { link_token }: { link_token: string }) {},
|
||||||
|
async updateTransactions() {},
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,5 +17,12 @@ export const queries = {
|
|||||||
return builder.users
|
return builder.users
|
||||||
.where('id', '=', authData.user.id)
|
.where('id', '=', authData.user.id)
|
||||||
.one();
|
.one();
|
||||||
})
|
}),
|
||||||
|
getPlaidLink: syncedQueryWithContext('getPlaidLink', z.tuple([]), (authData: AuthData | null) => {
|
||||||
|
isLoggedIn(authData);
|
||||||
|
return builder.plaidLink
|
||||||
|
.where('user_id', '=', authData.user.id)
|
||||||
|
.orderBy('createdAt', 'desc')
|
||||||
|
.one();
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { definePermissions } from "@rocicorp/zero";
|
import { definePermissions } from "@rocicorp/zero";
|
||||||
import { schema } from "./zero-schema.gen";
|
import { schema as schemaGen } from "./zero-schema.gen";
|
||||||
|
|
||||||
|
export const schema = schemaGen;
|
||||||
|
|
||||||
export const permissions = definePermissions(schema, () => ({}));
|
export const permissions = definePermissions(schema, () => ({}));
|
||||||
|
|||||||
@@ -21,6 +21,58 @@ type ZeroSchema = DrizzleToZeroSchema<typeof drizzleSchema>;
|
|||||||
*/
|
*/
|
||||||
export const schema = {
|
export const schema = {
|
||||||
tables: {
|
tables: {
|
||||||
|
plaidLink: {
|
||||||
|
name: "plaidLink",
|
||||||
|
columns: {
|
||||||
|
id: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
customType: null as unknown as ZeroCustomType<
|
||||||
|
ZeroSchema,
|
||||||
|
"plaidLink",
|
||||||
|
"id"
|
||||||
|
>,
|
||||||
|
},
|
||||||
|
user_id: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
customType: null as unknown as ZeroCustomType<
|
||||||
|
ZeroSchema,
|
||||||
|
"plaidLink",
|
||||||
|
"user_id"
|
||||||
|
>,
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
customType: null as unknown as ZeroCustomType<
|
||||||
|
ZeroSchema,
|
||||||
|
"plaidLink",
|
||||||
|
"link"
|
||||||
|
>,
|
||||||
|
},
|
||||||
|
token: {
|
||||||
|
type: "string",
|
||||||
|
optional: false,
|
||||||
|
customType: null as unknown as ZeroCustomType<
|
||||||
|
ZeroSchema,
|
||||||
|
"plaidLink",
|
||||||
|
"token"
|
||||||
|
>,
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: "number",
|
||||||
|
optional: true,
|
||||||
|
customType: null as unknown as ZeroCustomType<
|
||||||
|
ZeroSchema,
|
||||||
|
"plaidLink",
|
||||||
|
"createdAt"
|
||||||
|
>,
|
||||||
|
serverName: "created_at",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
primaryKey: ["id"],
|
||||||
|
},
|
||||||
transaction: {
|
transaction: {
|
||||||
name: "transaction",
|
name: "transaction",
|
||||||
columns: {
|
columns: {
|
||||||
@@ -147,6 +199,11 @@ export const schema = {
|
|||||||
* This type is auto-generated from your Drizzle schema definition.
|
* This type is auto-generated from your Drizzle schema definition.
|
||||||
*/
|
*/
|
||||||
export type Schema = typeof schema;
|
export type Schema = typeof schema;
|
||||||
|
/**
|
||||||
|
* Represents a row from the "plaidLink" table.
|
||||||
|
* This type is auto-generated from your Drizzle schema definition.
|
||||||
|
*/
|
||||||
|
export type PlaidLink = Row<Schema["tables"]["plaidLink"]>;
|
||||||
/**
|
/**
|
||||||
* Represents a row from the "transaction" table.
|
* Represents a row from the "transaction" table.
|
||||||
* This type is auto-generated from your Drizzle schema definition.
|
* This type is auto-generated from your Drizzle schema definition.
|
||||||
|
|||||||
Reference in New Issue
Block a user