Files
Chris Tate c1b1aaaddb custom schema system (#56)
* add rate limits

* fix lint

* better

generate prompt

update examples

* fixes

* fixes

* new api

* update docs

* fixes

* fixes

* generated prompt

* Add tests for catalog validation and prompt generation

- Test generateSystemPrompt with components, actions, custom rules
- Test new defineCatalog API from schema system
- Test catalog.prompt() method with custom rules
- Test catalog.validate() for valid and invalid specs
- Test catalog.zodSchema() for custom validation
- Test catalog.jsonSchema() for structured outputs
- Add tests for nested specs with children
- Add tests for rejecting invalid component types

* Fix lint: pass children as nested elements in renderer

Change from children={...} prop to nested children pattern
to satisfy react/no-children-prop rule.

* Fix lint errors in dashboard and web apps

- Disable react/prop-types in both eslint configs (TypeScript handles this)
- Allow styled-jsx 'jsx' property in dashboard
- Add DATABASE_URL to turbo env allowlist
- Remove unused drizzle-orm imports (and, sql)
- Suppress unused variable warnings where intentional

* Add server entry point for @json-render/remotion

The main package entry includes React components that require
client-side context (React.createContext). This causes build
failures when importing in server-side API routes.

Added `@json-render/remotion/server` entry point that exports
only schema and catalog definitions without React dependencies:
- schema, RemotionSchema, RemotionSpec
- standardComponentDefinitions, standardTransitionDefinitions
- standardEffectDefinitions
- Type exports for catalogs

Updated remotion example to import from /server in catalog.ts

* Make dashboard database connection lazy-initialized

The database connection threw an error at module load time if
DATABASE_URL was not set, causing builds to fail in CI where
the database is not available.

Changed to lazy initialization using a Proxy so the error only
occurs when the database is actually used at runtime, not at
build time.

* Fix previousSpec property name and lazy rate limiting

- Fix property name mismatch: playground now passes previousSpec
  instead of previousTree to match what useUIStream hook expects
- Make rate limiting lazy-initialized to avoid runtime errors when
  Redis env vars (KV_REST_API_URL, KV_REST_API_TOKEN) are not set
- Rate limiting gracefully becomes a no-op when Redis is unavailable
2026-02-05 03:59:56 -06:00

1110 lines
26 KiB
TypeScript

/**
* Database store for the dashboard API using Drizzle ORM
*/
import { eq, ilike, or, desc, asc, max } from "drizzle-orm";
import { db } from "./connection";
import {
customers,
invoices,
expenses,
accounts,
transactions,
widgets,
type Customer,
type Invoice,
type Expense,
type InvoiceItem,
type WidgetSpec,
} from "./schema";
// Helper to generate IDs
const generateId = (prefix: string) => `${prefix}-${Date.now()}`;
// Customer methods
export async function getCustomers(filters?: {
status?: string;
search?: string;
limit?: number;
sort?: "newest" | "oldest";
}) {
let query = db.select().from(customers);
if (filters?.status) {
query = query.where(
eq(customers.status, filters.status as "active" | "inactive"),
) as typeof query;
}
if (filters?.search) {
const searchPattern = `%${filters.search}%`;
query = query.where(
or(
ilike(customers.name, searchPattern),
ilike(customers.email, searchPattern),
),
) as typeof query;
}
// Default to newest first
const sortOrder = filters?.sort === "oldest" ? asc : desc;
query = query.orderBy(sortOrder(customers.createdAt)) as typeof query;
if (filters?.limit) {
query = query.limit(filters.limit) as typeof query;
}
return query;
}
export async function getCustomer(id: string) {
const result = await db
.select()
.from(customers)
.where(eq(customers.id, id))
.limit(1);
return result[0] || null;
}
export async function createCustomer(data: {
name: string;
email: string;
phone?: string;
}) {
const id = generateId("cust");
const [customer] = await db
.insert(customers)
.values({
id,
name: data.name,
email: data.email,
phone: data.phone || null,
balance: "0",
status: "active",
})
.returning();
return customer;
}
export async function updateCustomer(id: string, data: Partial<Customer>) {
const [customer] = await db
.update(customers)
.set(data)
.where(eq(customers.id, id))
.returning();
return customer || null;
}
export async function deleteCustomer(id: string) {
const result = await db
.delete(customers)
.where(eq(customers.id, id))
.returning({ id: customers.id });
return { success: result.length > 0 };
}
// Invoice methods
export async function getInvoices(filters?: {
status?: string;
customerId?: string;
}) {
let query = db.select().from(invoices);
if (filters?.status) {
query = query.where(
eq(invoices.status, filters.status as Invoice["status"]),
) as typeof query;
}
if (filters?.customerId) {
query = query.where(
eq(invoices.customerId, filters.customerId),
) as typeof query;
}
return query;
}
export async function getInvoice(id: string) {
const result = await db
.select()
.from(invoices)
.where(eq(invoices.id, id))
.limit(1);
return result[0] || null;
}
export async function createInvoice(data: {
customerId: string;
dueDate: string;
items?: InvoiceItem[];
}) {
const customer = await getCustomer(data.customerId);
if (!customer) return null;
const items = data.items || [];
const amount = items.reduce((sum, item) => sum + item.amount, 0);
const id = generateId("inv");
const [invoice] = await db
.insert(invoices)
.values({
id,
customerId: data.customerId,
customerName: customer.name,
amount: amount.toString(),
status: "draft",
dueDate: new Date(data.dueDate),
items,
})
.returning();
return invoice;
}
export async function updateInvoice(id: string, data: Partial<Invoice>) {
const [invoice] = await db
.update(invoices)
.set(data)
.where(eq(invoices.id, id))
.returning();
return invoice || null;
}
export async function deleteInvoice(id: string) {
const result = await db
.delete(invoices)
.where(eq(invoices.id, id))
.returning({ id: invoices.id });
return result.length > 0;
}
export async function sendInvoice(id: string) {
const invoice = await getInvoice(id);
if (!invoice) return { error: "Invoice not found" };
if (invoice.status !== "draft")
return { error: "Only draft invoices can be sent" };
return updateInvoice(id, { status: "sent" });
}
export async function markInvoicePaid(id: string) {
const invoice = await getInvoice(id);
if (!invoice) return { error: "Invoice not found" };
if (invoice.status === "paid") return { error: "Invoice is already paid" };
// Update customer balance
const customer = await getCustomer(invoice.customerId);
if (customer) {
const newBalance =
parseFloat(customer.balance as string) -
parseFloat(invoice.amount as string);
await updateCustomer(invoice.customerId, {
balance: newBalance.toString(),
});
}
// Add transaction
await db.insert(transactions).values({
id: generateId("txn"),
accountId: "acc-001",
type: "income",
amount: invoice.amount,
description: `Invoice #${invoice.id} payment from ${invoice.customerName}`,
category: "Sales",
date: new Date(),
});
return updateInvoice(id, { status: "paid" });
}
// Expense methods
export async function getExpenses(filters?: {
status?: string;
category?: string;
}) {
let query = db.select().from(expenses);
if (filters?.status) {
query = query.where(
eq(expenses.status, filters.status as Expense["status"]),
) as typeof query;
}
if (filters?.category) {
query = query.where(
eq(expenses.category, filters.category),
) as typeof query;
}
return query;
}
export async function getExpense(id: string) {
const result = await db
.select()
.from(expenses)
.where(eq(expenses.id, id))
.limit(1);
return result[0] || null;
}
export async function createExpense(data: {
vendor: string;
category: string;
amount: number;
date?: string;
description?: string;
}) {
const id = generateId("exp");
const [expense] = await db
.insert(expenses)
.values({
id,
vendor: data.vendor,
category: data.category,
amount: data.amount.toString(),
date: data.date ? new Date(data.date) : new Date(),
status: "pending",
description: data.description || null,
})
.returning();
return expense;
}
export async function updateExpense(id: string, data: Partial<Expense>) {
const [expense] = await db
.update(expenses)
.set(data)
.where(eq(expenses.id, id))
.returning();
return expense || null;
}
export async function deleteExpense(id: string) {
const result = await db
.delete(expenses)
.where(eq(expenses.id, id))
.returning({ id: expenses.id });
return result.length > 0;
}
export async function approveExpense(id: string) {
const expense = await getExpense(id);
if (!expense) return { error: "Expense not found" };
if (expense.status !== "pending")
return { error: "Only pending expenses can be approved" };
// Add transaction
await db.insert(transactions).values({
id: generateId("txn"),
accountId: "acc-001",
type: "expense",
amount: expense.amount,
description: `${expense.vendor} - ${expense.description || ""}`,
category: expense.category,
date: expense.date,
});
return updateExpense(id, { status: "approved" });
}
export async function rejectExpense(id: string) {
const expense = await getExpense(id);
if (!expense) return { error: "Expense not found" };
if (expense.status !== "pending")
return { error: "Only pending expenses can be rejected" };
return updateExpense(id, { status: "rejected" });
}
// Account methods
export async function getAccounts() {
const accountList = await db.select().from(accounts);
// Get recent transactions for each account
const result = await Promise.all(
accountList.map(async (account) => {
const recentTxns = await db
.select()
.from(transactions)
.where(eq(transactions.accountId, account.id))
.orderBy(desc(transactions.date))
.limit(5);
return { ...account, recentTransactions: recentTxns };
}),
);
return result;
}
export async function getAccount(id: string) {
const result = await db
.select()
.from(accounts)
.where(eq(accounts.id, id))
.limit(1);
if (!result[0]) return null;
const txns = await db
.select()
.from(transactions)
.where(eq(transactions.accountId, id))
.orderBy(desc(transactions.date));
return { ...result[0], transactions: txns };
}
// Dashboard summary
export async function getDashboardSummary() {
// Get all invoices
const allInvoices = await db.select().from(invoices);
const paidInvoices = allInvoices.filter((i) => i.status === "paid");
const totalRevenue = paidInvoices.reduce(
(sum, i) => sum + parseFloat(i.amount as string),
0,
);
const outstandingInvoices = allInvoices
.filter((i) => i.status === "sent" || i.status === "overdue")
.reduce((sum, i) => sum + parseFloat(i.amount as string), 0);
const overdueAmount = allInvoices
.filter((i) => i.status === "overdue")
.reduce((sum, i) => sum + parseFloat(i.amount as string), 0);
// Get all expenses
const allExpenses = await db.select().from(expenses);
const approvedExpenses = allExpenses.filter((e) => e.status === "approved");
const totalExpenses = approvedExpenses.reduce(
(sum, e) => sum + parseFloat(e.amount as string),
0,
);
const cashFlow = totalRevenue - totalExpenses;
// Get accounts
const allAccounts = await db.select().from(accounts);
const totalBankBalance = allAccounts
.filter((a) => a.type === "bank")
.reduce((sum, a) => sum + parseFloat(a.balance as string), 0);
// Get customers
const allCustomers = await db.select().from(customers);
// Get recent transactions
const recentTxns = await db
.select()
.from(transactions)
.orderBy(desc(transactions.date))
.limit(5);
// Group expenses by category
const expensesByCategory = approvedExpenses.reduce(
(acc, e) => {
acc[e.category] = (acc[e.category] || 0) + parseFloat(e.amount as string);
return acc;
},
{} as Record<string, number>,
);
return {
revenue: {
total: totalRevenue,
outstanding: outstandingInvoices,
overdue: overdueAmount,
},
expenses: {
total: totalExpenses,
pending: allExpenses.filter((e) => e.status === "pending").length,
},
cashFlow,
bankBalance: totalBankBalance,
customers: {
total: allCustomers.length,
active: allCustomers.filter((c) => c.status === "active").length,
},
invoices: {
total: allInvoices.length,
draft: allInvoices.filter((i) => i.status === "draft").length,
sent: allInvoices.filter((i) => i.status === "sent").length,
paid: allInvoices.filter((i) => i.status === "paid").length,
overdue: allInvoices.filter((i) => i.status === "overdue").length,
},
recentTransactions: recentTxns.map((t) => ({
...t,
amount: parseFloat(t.amount as string),
})),
expensesByCategory: Object.entries(expensesByCategory).map(
([label, value]) => ({
label,
value,
}),
),
revenueByMonth: [
{ label: "Oct", value: 18000 },
{ label: "Nov", value: 22000 },
{ label: "Dec", value: 28000 },
{ label: "Jan", value: 35000 },
{ label: "Feb", value: totalRevenue },
],
};
}
// Reports
export async function getProfitLossReport(
startDate?: string,
endDate?: string,
) {
const start = startDate ? new Date(startDate) : new Date("2024-01-01");
const end = endDate ? new Date(endDate) : new Date("2024-12-31");
const allInvoices = await db.select().from(invoices);
const paidInvoices = allInvoices.filter(
(i) => i.status === "paid" && i.createdAt >= start && i.createdAt <= end,
);
const allExpenses = await db.select().from(expenses);
const approvedExpenses = allExpenses.filter(
(e) => e.status === "approved" && e.date >= start && e.date <= end,
);
const totalIncome = paidInvoices.reduce(
(sum, i) => sum + parseFloat(i.amount as string),
0,
);
const totalExpensesAmount = approvedExpenses.reduce(
(sum, e) => sum + parseFloat(e.amount as string),
0,
);
const netProfit = totalIncome - totalExpensesAmount;
const expensesByCategory = approvedExpenses.reduce(
(acc, e) => {
acc[e.category] = (acc[e.category] || 0) + parseFloat(e.amount as string);
return acc;
},
{} as Record<string, number>,
);
return {
period: {
startDate: start.toISOString().split("T")[0],
endDate: end.toISOString().split("T")[0],
},
income: {
total: totalIncome,
invoiceCount: paidInvoices.length,
},
expenses: {
total: totalExpensesAmount,
expenseCount: approvedExpenses.length,
byCategory: Object.entries(expensesByCategory).map(
([category, amount]) => ({
category,
amount,
}),
),
},
netProfit,
profitMargin: totalIncome > 0 ? (netProfit / totalIncome) * 100 : 0,
};
}
// Clear database (delete all data)
export async function clearDatabase() {
await db.delete(transactions);
await db.delete(invoices);
await db.delete(expenses);
await db.delete(customers);
await db.delete(accounts);
return { message: "Database cleared successfully" };
}
// Reset/seed database
export async function resetDatabase() {
// Delete all data
await clearDatabase();
// Seed accounts first
await db.insert(accounts).values([
{
id: "acc-001",
name: "Business Checking",
type: "bank",
balance: "125000",
lastSync: new Date(),
},
{
id: "acc-002",
name: "Business Savings",
type: "bank",
balance: "75000",
lastSync: new Date(),
},
{
id: "acc-003",
name: "Corporate Card",
type: "credit_card",
balance: "-8500",
lastSync: new Date(),
},
{
id: "acc-004",
name: "Petty Cash",
type: "cash",
balance: "500",
lastSync: new Date(),
},
]);
// Seed customers with dates spread over past 30 days
const now = new Date();
const daysAgo = (days: number) =>
new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
const seedCustomers = [
// Enterprise customers (older)
{
id: "cust-001",
name: "Acme Corporation",
email: "billing@acme.com",
phone: "(555) 123-4567",
balance: "15000",
status: "active" as const,
createdAt: daysAgo(28),
},
{
id: "cust-002",
name: "Globex Industries",
email: "ap@globex.com",
phone: "(555) 234-5678",
balance: "8500",
status: "active" as const,
createdAt: daysAgo(27),
},
{
id: "cust-003",
name: "Initech LLC",
email: "finance@initech.com",
phone: "(555) 345-6789",
balance: "2300",
status: "active" as const,
createdAt: daysAgo(25),
},
{
id: "cust-004",
name: "Umbrella Corp",
email: "accounts@umbrella.com",
phone: "(555) 456-7890",
balance: "45000",
status: "active" as const,
createdAt: daysAgo(24),
},
{
id: "cust-005",
name: "Stark Industries",
email: "billing@stark.com",
phone: "(555) 567-8901",
balance: "0",
status: "inactive" as const,
createdAt: daysAgo(23),
},
// Week 3 signups
{
id: "cust-006",
name: "Wayne Enterprises",
email: "finance@wayne.com",
phone: "(555) 678-9012",
balance: "32000",
status: "active" as const,
createdAt: daysAgo(21),
},
{
id: "cust-007",
name: "Oscorp",
email: "billing@oscorp.com",
phone: "(555) 789-0123",
balance: "12500",
status: "active" as const,
createdAt: daysAgo(20),
},
{
id: "cust-008",
name: "LexCorp",
email: "accounts@lexcorp.com",
phone: "(555) 890-1234",
balance: "67000",
status: "active" as const,
createdAt: daysAgo(19),
},
{
id: "cust-009",
name: "Cyberdyne Systems",
email: "ap@cyberdyne.com",
phone: "(555) 901-2345",
balance: "4500",
status: "active" as const,
createdAt: daysAgo(18),
},
{
id: "cust-010",
name: "Weyland-Yutani",
email: "billing@weyland.com",
phone: "(555) 012-3456",
balance: "89000",
status: "active" as const,
createdAt: daysAgo(17),
},
// Week 2 signups (more activity)
{
id: "cust-011",
name: "Tyrell Corp",
email: "finance@tyrell.com",
phone: "(555) 111-2222",
balance: "23000",
status: "active" as const,
createdAt: daysAgo(14),
},
{
id: "cust-012",
name: "Soylent Corp",
email: "billing@soylent.com",
phone: "(555) 222-3333",
balance: "5600",
status: "active" as const,
createdAt: daysAgo(14),
},
{
id: "cust-013",
name: "Massive Dynamic",
email: "accounts@massive.com",
phone: "(555) 333-4444",
balance: "41000",
status: "active" as const,
createdAt: daysAgo(13),
},
{
id: "cust-014",
name: "Aperture Science",
email: "billing@aperture.com",
phone: "(555) 444-5555",
balance: "0",
status: "inactive" as const,
createdAt: daysAgo(12),
},
{
id: "cust-015",
name: "Black Mesa",
email: "finance@blackmesa.com",
phone: "(555) 555-6666",
balance: "18500",
status: "active" as const,
createdAt: daysAgo(11),
},
{
id: "cust-016",
name: "Vault-Tec",
email: "sales@vaulttec.com",
phone: "(555) 666-7777",
balance: "72000",
status: "active" as const,
createdAt: daysAgo(10),
},
{
id: "cust-017",
name: "Abstergo Industries",
email: "billing@abstergo.com",
phone: "(555) 777-8888",
balance: "9800",
status: "active" as const,
createdAt: daysAgo(10),
},
{
id: "cust-018",
name: "Shinra Electric",
email: "accounts@shinra.com",
phone: "(555) 888-9999",
balance: "125000",
status: "active" as const,
createdAt: daysAgo(9),
},
// Week 1 signups (high activity)
{
id: "cust-019",
name: "Monsters Inc",
email: "billing@monsters.com",
phone: "(555) 999-0000",
balance: "34500",
status: "active" as const,
createdAt: daysAgo(7),
},
{
id: "cust-020",
name: "Buy n Large",
email: "finance@bnl.com",
phone: "(555) 000-1111",
balance: "56000",
status: "active" as const,
createdAt: daysAgo(7),
},
{
id: "cust-021",
name: "Omni Consumer",
email: "ap@omni.com",
phone: "(555) 121-2121",
balance: "8900",
status: "active" as const,
createdAt: daysAgo(6),
},
{
id: "cust-022",
name: "Rekall Inc",
email: "billing@rekall.com",
phone: "(555) 131-3131",
balance: "15600",
status: "active" as const,
createdAt: daysAgo(5),
},
{
id: "cust-023",
name: "Virtucon",
email: "accounts@virtucon.com",
phone: "(555) 141-4141",
balance: "28000",
status: "active" as const,
createdAt: daysAgo(5),
},
{
id: "cust-024",
name: "Wonka Industries",
email: "finance@wonka.com",
phone: "(555) 151-5151",
balance: "47000",
status: "active" as const,
createdAt: daysAgo(4),
},
{
id: "cust-025",
name: "Dunder Mifflin",
email: "sales@dundermifflin.com",
phone: "(555) 161-6161",
balance: "3200",
status: "active" as const,
createdAt: daysAgo(4),
},
{
id: "cust-026",
name: "Sterling Cooper",
email: "billing@sterlingcooper.com",
phone: "(555) 171-7171",
balance: "19500",
status: "active" as const,
createdAt: daysAgo(3),
},
// Recent signups (last few days)
{
id: "cust-027",
name: "Prestige Worldwide",
email: "boats@prestige.com",
phone: "(555) 181-8181",
balance: "0",
status: "active" as const,
createdAt: daysAgo(2),
},
{
id: "cust-028",
name: "Pied Piper",
email: "richard@piedpiper.com",
phone: "(555) 191-9191",
balance: "500",
status: "active" as const,
createdAt: daysAgo(2),
},
{
id: "cust-029",
name: "Hooli",
email: "billing@hooli.com",
phone: "(555) 202-0202",
balance: "250000",
status: "active" as const,
createdAt: daysAgo(1),
},
{
id: "cust-030",
name: "Aviato",
email: "erlich@aviato.com",
phone: "(555) 212-1212",
balance: "1200",
status: "active" as const,
createdAt: daysAgo(1),
},
{
id: "cust-031",
name: "Bluth Company",
email: "gob@bluth.com",
phone: "(555) 222-2222",
balance: "0",
status: "inactive" as const,
createdAt: daysAgo(0),
},
{
id: "cust-032",
name: "Los Pollos Hermanos",
email: "gus@lospollos.com",
phone: "(555) 232-3232",
balance: "98000",
status: "active" as const,
createdAt: daysAgo(0),
},
];
await db.insert(customers).values(seedCustomers);
// Seed invoices
await db.insert(invoices).values([
{
id: "inv-001",
customerId: "cust-001",
customerName: "Acme Corporation",
amount: "5000",
status: "paid",
dueDate: new Date("2024-02-15"),
items: [
{
description: "Consulting Services",
quantity: 20,
rate: 250,
amount: 5000,
},
],
},
{
id: "inv-002",
customerId: "cust-002",
customerName: "Globex Industries",
amount: "3500",
status: "sent",
dueDate: new Date("2024-02-28"),
items: [
{
description: "Software License",
quantity: 7,
rate: 500,
amount: 3500,
},
],
},
{
id: "inv-003",
customerId: "cust-001",
customerName: "Acme Corporation",
amount: "10000",
status: "overdue",
dueDate: new Date("2024-01-31"),
items: [
{
description: "Annual Support Contract",
quantity: 1,
rate: 10000,
amount: 10000,
},
],
},
{
id: "inv-004",
customerId: "cust-003",
customerName: "Initech LLC",
amount: "2300",
status: "draft",
dueDate: new Date("2024-03-15"),
items: [
{
description: "Training Session",
quantity: 1,
rate: 2300,
amount: 2300,
},
],
},
{
id: "inv-005",
customerId: "cust-004",
customerName: "Umbrella Corp",
amount: "45000",
status: "sent",
dueDate: new Date("2024-03-01"),
items: [
{
description: "Enterprise License",
quantity: 1,
rate: 45000,
amount: 45000,
},
],
},
]);
// Seed expenses
await db.insert(expenses).values([
{
id: "exp-001",
vendor: "AWS",
category: "Software",
amount: "2500",
date: new Date("2024-02-01"),
status: "approved",
description: "Cloud hosting - February",
},
{
id: "exp-002",
vendor: "Office Depot",
category: "Office Supplies",
amount: "350",
date: new Date("2024-02-05"),
status: "approved",
description: "Printer paper and toner",
},
{
id: "exp-003",
vendor: "Delta Airlines",
category: "Travel",
amount: "890",
date: new Date("2024-02-10"),
status: "pending",
description: "Flight to client meeting",
},
{
id: "exp-004",
vendor: "WeWork",
category: "Rent",
amount: "4500",
date: new Date("2024-02-01"),
status: "approved",
description: "Office space - February",
},
{
id: "exp-005",
vendor: "Uber",
category: "Travel",
amount: "125",
date: new Date("2024-02-12"),
status: "pending",
description: "Client site visits",
},
]);
// Seed transactions
await db.insert(transactions).values([
{
id: "txn-001",
accountId: "acc-001",
type: "income",
amount: "5000",
description: "Invoice #inv-001 payment",
category: "Sales",
date: new Date("2024-02-10"),
},
{
id: "txn-002",
accountId: "acc-001",
type: "expense",
amount: "2500",
description: "AWS hosting",
category: "Software",
date: new Date("2024-02-01"),
},
{
id: "txn-003",
accountId: "acc-003",
type: "expense",
amount: "350",
description: "Office supplies",
category: "Office",
date: new Date("2024-02-05"),
},
{
id: "txn-004",
accountId: "acc-001",
type: "income",
amount: "15000",
description: "Consulting retainer",
category: "Sales",
date: new Date("2024-02-01"),
},
{
id: "txn-005",
accountId: "acc-001",
type: "expense",
amount: "4500",
description: "Office rent",
category: "Rent",
date: new Date("2024-02-01"),
},
]);
return { message: "Database reset and seeded successfully" };
}
// Widget methods
export async function getWidgets() {
return db.select().from(widgets).orderBy(asc(widgets.order));
}
export async function getWidget(id: string) {
const result = await db
.select()
.from(widgets)
.where(eq(widgets.id, id))
.limit(1);
return result[0] || null;
}
export async function createWidget(data: { prompt: string; spec: WidgetSpec }) {
const id = generateId("widget");
// Get the next order value
const [result] = await db
.select({ maxOrder: max(widgets.order) })
.from(widgets);
const nextOrder = (result?.maxOrder ?? -1) + 1;
const [widget] = await db
.insert(widgets)
.values({
id,
prompt: data.prompt,
spec: data.spec,
order: nextOrder,
})
.returning();
return widget;
}
export async function updateWidget(
id: string,
data: { prompt?: string; spec?: WidgetSpec },
) {
const [widget] = await db
.update(widgets)
.set({ ...data, updatedAt: new Date() })
.where(eq(widgets.id, id))
.returning();
return widget || null;
}
export async function deleteWidget(id: string) {
const result = await db
.delete(widgets)
.where(eq(widgets.id, id))
.returning({ id: widgets.id });
return result.length > 0;
}
export async function reorderWidgets(orderedIds: string[]) {
// Update each widget's order based on position in array
for (let i = 0; i < orderedIds.length; i++) {
const id = orderedIds[i];
if (!id) continue;
await db.update(widgets).set({ order: i }).where(eq(widgets.id, id));
}
}