style: apply Biome formatting to TypeScript files (#27)

- Fix import sorting
- Standardize code formatting
- Apply consistent TypeScript style
- Update code to match Biome configuration

Part of #27
This commit is contained in:
Peter Wood
2025-05-04 09:55:01 -04:00
parent f2c0373640
commit 58d8ebdfa1
9 changed files with 214 additions and 211 deletions

View File

@@ -1,12 +1,11 @@
import React, { useState, useEffect } from 'react';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import type React from 'react';
import { useEffect, useState } from 'react';
import { currentAccountId as currentAccountIdStore, refreshKey } from '../stores/transactionStore';
import type { Account } from '../types'; import type { Account } from '../types';
import { formatCurrency } from '../utils'; import { formatCurrency } from '../utils';
import { currentAccountId as currentAccountIdStore, refreshKey } from '../stores/transactionStore';
interface AccountSummaryProps { type AccountSummaryProps = {};
// No props needed, data comes from store and fetch
}
export default function AccountSummary({}: AccountSummaryProps) { export default function AccountSummary({}: AccountSummaryProps) {
const currentAccountId = useStore(currentAccountIdStore); const currentAccountId = useStore(currentAccountIdStore);

View File

@@ -1,16 +1,17 @@
import React, { useState, useEffect } from 'react';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import type { Transaction } from '../types'; import type React from 'react';
import { useEffect, useState } from 'react';
// Import store atoms and actions // Import store atoms and actions
import { import {
currentAccountId as currentAccountIdStore,
transactionToEdit as transactionToEditStore,
cancelEditingTransaction, cancelEditingTransaction,
currentAccountId as currentAccountIdStore,
transactionSaved, transactionSaved,
transactionToEdit as transactionToEditStore,
} from '../stores/transactionStore'; } from '../stores/transactionStore';
import type { Transaction } from '../types';
// Remove props that now come from the store // Remove props that now come from the store
interface AddTransactionFormProps {} type AddTransactionFormProps = {};
export default function AddTransactionForm({}: AddTransactionFormProps) { export default function AddTransactionForm({}: AddTransactionFormProps) {
// --- Read state from store --- // --- Read state from store ---
@@ -87,7 +88,7 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
if (!amount) { if (!amount) {
errors.push('Amount is required'); errors.push('Amount is required');
} else { } else {
const amountNum = parseFloat(amount); const amountNum = Number.parseFloat(amount);
if (isNaN(amountNum)) { if (isNaN(amountNum)) {
errors.push('Amount must be a valid number'); errors.push('Amount must be a valid number');
} else if (amountNum === 0) { } else if (amountNum === 0) {
@@ -134,7 +135,7 @@ export default function AddTransactionForm({}: AddTransactionFormProps) {
accountId: currentAccountId, accountId: currentAccountId,
date: date, // Send as YYYY-MM-DD string date: date, // Send as YYYY-MM-DD string
description: description.trim(), description: description.trim(),
amount: parseFloat(amount), amount: Number.parseFloat(amount),
}; };
const method = editingId ? 'PUT' : 'POST'; const method = editingId ? 'PUT' : 'POST';

View File

@@ -1,15 +1,15 @@
import React, { useState, useEffect } from 'react';
import { useStore } from '@nanostores/react'; import { useStore } from '@nanostores/react';
import React, { useState, useEffect } from 'react';
import {
currentAccountId as currentAccountIdStore,
refreshKey,
startEditingTransaction,
triggerRefresh,
} from '../stores/transactionStore';
import type { Transaction } from '../types'; import type { Transaction } from '../types';
import { formatCurrency, formatDate } from '../utils'; import { formatCurrency, formatDate } from '../utils';
import {
startEditingTransaction,
currentAccountId as currentAccountIdStore,
triggerRefresh,
refreshKey,
} from '../stores/transactionStore';
interface TransactionTableProps {} type TransactionTableProps = {};
export default function TransactionTable({}: TransactionTableProps) { export default function TransactionTable({}: TransactionTableProps) {
const currentAccountId = useStore(currentAccountIdStore); const currentAccountId = useStore(currentAccountIdStore);
@@ -47,7 +47,7 @@ export default function TransactionTable({}: TransactionTableProps) {
}, [currentAccountId, refreshCounter]); }, [currentAccountId, refreshCounter]);
const sortedTransactions = [...transactions].sort( const sortedTransactions = [...transactions].sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
); );
const handleDelete = async (txnId: string) => { const handleDelete = async (txnId: string) => {
@@ -76,7 +76,7 @@ export default function TransactionTable({}: TransactionTableProps) {
console.log(`Transaction ${txnId} deleted successfully.`); console.log(`Transaction ${txnId} deleted successfully.`);
setTransactions((currentTransactions) => setTransactions((currentTransactions) =>
currentTransactions.filter((txn) => txn.id !== txnId) currentTransactions.filter((txn) => txn.id !== txnId),
); );
triggerRefresh(); triggerRefresh();

View File

@@ -1,5 +1,5 @@
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
import { transactions, accounts } from '../../../../data/store'; import { accounts, transactions } from '../../../../data/store';
import type { Transaction } from '../../../../types'; import type { Transaction } from '../../../../types';
export const PUT: APIRoute = async ({ request, params }) => { export const PUT: APIRoute = async ({ request, params }) => {

View File

@@ -11,7 +11,7 @@
*/ */
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
import { transactions, accounts } from '../../../data/store'; import { accounts, transactions } from '../../../data/store';
import type { Transaction } from '../../../types'; import type { Transaction } from '../../../types';
/** /**

View File

@@ -1,11 +1,11 @@
// src/server.ts // src/server.ts
import Fastify, { FastifyInstance } from 'fastify'; import { IncomingMessage, Server, ServerResponse } from 'http';
import { Server, IncomingMessage, ServerResponse } from 'http'; import { type BankAccount, PrismaClient } from '@prisma/client';
import { PrismaClient, BankAccount } from '@prisma/client';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import Fastify, { type FastifyInstance } from 'fastify';
import type { ZodTypeProvider } from 'fastify-type-provider-zod';
import { z } from 'zod'; import { z } from 'zod';
import { ZodTypeProvider } from 'fastify-type-provider-zod';
dotenv.config(); dotenv.config();
@@ -13,9 +13,9 @@ const prisma = new PrismaClient();
// Base schema for common fields, useful for reuse // Base schema for common fields, useful for reuse
const bankAccountBaseSchema = z.object({ const bankAccountBaseSchema = z.object({
name: z.string().min(1, { message: "Name cannot be empty" }), name: z.string().min(1, { message: 'Name cannot be empty' }),
bankName: z.string().min(1, { message: "Bank name cannot be empty" }), bankName: z.string().min(1, { message: 'Bank name cannot be empty' }),
accountNumber: z.string().min(1, { message: "Account number cannot be empty" }), accountNumber: z.string().min(1, { message: 'Account number cannot be empty' }),
}); });
// Schema for creating a bank account (all fields required) // Schema for creating a bank account (all fields required)
@@ -23,8 +23,8 @@ const createBankAccountSchema = bankAccountBaseSchema;
// Schema for request parameters containing an ID // Schema for request parameters containing an ID
const paramsSchema = z.object({ const paramsSchema = z.object({
// Use coerce to automatically convert string param to number // Use coerce to automatically convert string param to number
id: z.coerce.number().int().positive({ message: "ID must be a positive integer" }) id: z.coerce.number().int().positive({ message: 'ID must be a positive integer' }),
}); });
// Schema for updating a bank account (all fields optional) // Schema for updating a bank account (all fields optional)
@@ -32,7 +32,7 @@ const updateBankAccountSchema = bankAccountBaseSchema.partial(); // Makes all fi
// --- Fastify Server Instance with Zod Type Provider --- // --- Fastify Server Instance with Zod Type Provider ---
const server: FastifyInstance = Fastify({ const server: FastifyInstance = Fastify({
logger: true logger: true,
}).withTypeProvider<ZodTypeProvider>(); // Enable Zod validation and typing }).withTypeProvider<ZodTypeProvider>(); // Enable Zod validation and typing
// --- API Routes --- // --- API Routes ---
@@ -41,206 +41,209 @@ const API_PREFIX = '/api/bank-account';
// 1. Create Bank Account // 1. Create Bank Account
server.post( server.post(
`${API_PREFIX}/create`, `${API_PREFIX}/create`,
{ {
schema: { // Define Zod schema for the request body schema: {
body: createBankAccountSchema // Define Zod schema for the request body
} body: createBankAccountSchema,
}, },
async (request, reply): Promise<BankAccount> => { },
try { async (request, reply): Promise<BankAccount> => {
// request.body is now typed and validated by Zod! try {
const newAccount = await prisma.bankAccount.create({ // request.body is now typed and validated by Zod!
data: request.body, // Pass validated body directly const newAccount = await prisma.bankAccount.create({
}); data: request.body, // Pass validated body directly
reply.code(201); });
return newAccount; reply.code(201);
} catch (error: any) { return newAccount;
server.log.error(error); } catch (error: any) {
if (error.code === 'P2002' && error.meta?.target?.includes('accountNumber')) { server.log.error(error);
reply.code(409); if (error.code === 'P2002' && error.meta?.target?.includes('accountNumber')) {
const body = createBankAccountSchema.parse(request.body); reply.code(409);
throw new Error(`Bank account with number ${body.accountNumber} already exists.`); const body = createBankAccountSchema.parse(request.body);
} throw new Error(`Bank account with number ${body.accountNumber} already exists.`);
reply.code(500); }
throw new Error('Failed to create bank account.'); reply.code(500);
} throw new Error('Failed to create bank account.');
} }
},
); );
// 2. Update Bank Account // 2. Update Bank Account
server.post( server.post(
`${API_PREFIX}/update/:id`, `${API_PREFIX}/update/:id`,
{ {
schema: { // Define Zod schemas for params and body schema: {
params: paramsSchema, // Define Zod schemas for params and body
body: updateBankAccountSchema params: paramsSchema,
} body: updateBankAccountSchema,
}, },
async (request, reply): Promise<BankAccount> => { },
try { async (request, reply): Promise<BankAccount> => {
// request.params.id is now a validated number try {
// request.body is now a validated partial object // request.params.id is now a validated number
const { id } = request.params; // request.body is now a validated partial object
const updateData = request.body; const { id } = request.params;
const updateData = request.body;
// Prevent updating with an empty object // Prevent updating with an empty object
if (Object.keys(updateData).length === 0) { if (Object.keys(updateData).length === 0) {
reply.code(400); reply.code(400);
throw new Error("Request body cannot be empty for update."); throw new Error('Request body cannot be empty for update.');
} }
const updatedAccount = await prisma.bankAccount.update({ const updatedAccount = await prisma.bankAccount.update({
where: { id: id }, // Use the validated numeric ID where: { id: id }, // Use the validated numeric ID
data: updateData, data: updateData,
}); });
return updatedAccount; return updatedAccount;
} catch (error: any) { } catch (error: any) {
server.log.error(error); server.log.error(error);
if (error.code === 'P2025') { // Record to update not found if (error.code === 'P2025') {
reply.code(404); // Record to update not found
throw new Error(`Bank account with ID ${request.params.id} not found.`); reply.code(404);
} throw new Error(`Bank account with ID ${request.params.id} not found.`);
if (error.code === 'P2002' && error.meta?.target?.includes('accountNumber')) { }
reply.code(409); if (error.code === 'P2002' && error.meta?.target?.includes('accountNumber')) {
// Access accountNumber safely as it's optional in update reply.code(409);
const attemptedNumber = request.body.accountNumber || '(unchanged)'; // Access accountNumber safely as it's optional in update
throw new Error(`Bank account with number ${attemptedNumber} already exists.`); const attemptedNumber = request.body.accountNumber || '(unchanged)';
} throw new Error(`Bank account with number ${attemptedNumber} already exists.`);
// Handle Zod validation errors specifically if needed (though Fastify usually does) }
if (error instanceof z.ZodError) { // Handle Zod validation errors specifically if needed (though Fastify usually does)
reply.code(400); if (error instanceof z.ZodError) {
throw new Error(`Validation Error: ${error.errors.map(e => e.message).join(', ')}`); reply.code(400);
} throw new Error(`Validation Error: ${error.errors.map((e) => e.message).join(', ')}`);
reply.code(500); }
throw new Error('Failed to update bank account.'); reply.code(500);
} throw new Error('Failed to update bank account.');
} }
},
); );
// 3. Delete Bank Account // 3. Delete Bank Account
server.delete( server.delete(
`${API_PREFIX}/delete/:id`, `${API_PREFIX}/delete/:id`,
{ {
schema: { // Define Zod schema for params schema: {
params: paramsSchema // Define Zod schema for params
} params: paramsSchema,
}, },
async (request, reply): Promise<{ message: string; deletedAccount: BankAccount }> => { },
try { async (request, reply): Promise<{ message: string; deletedAccount: BankAccount }> => {
// request.params.id is now a validated number try {
const { id } = request.params; // request.params.id is now a validated number
const { id } = request.params;
const deletedAccount = await prisma.bankAccount.delete({ const deletedAccount = await prisma.bankAccount.delete({
where: { id: id }, // Use the validated numeric ID where: { id: id }, // Use the validated numeric ID
}); });
return { message: `Bank account with ID ${id} deleted successfully.`, deletedAccount }; return { message: `Bank account with ID ${id} deleted successfully.`, deletedAccount };
} catch (error: any) { } catch (error: any) {
server.log.error(error); server.log.error(error);
if (error.code === 'P2025') { // Record to delete not found if (error.code === 'P2025') {
reply.code(404); // Record to delete not found
throw new Error(`Bank account with ID ${request.params.id} not found.`); reply.code(404);
} throw new Error(`Bank account with ID ${request.params.id} not found.`);
// Handle Zod validation errors }
if (error instanceof z.ZodError) { // Handle Zod validation errors
reply.code(400); if (error instanceof z.ZodError) {
throw new Error(`Validation Error: ${error.errors.map(e => e.message).join(', ')}`); reply.code(400);
} throw new Error(`Validation Error: ${error.errors.map((e) => e.message).join(', ')}`);
reply.code(500); }
throw new Error('Failed to delete bank account.'); reply.code(500);
} throw new Error('Failed to delete bank account.');
} }
},
); );
// 4. Get All Bank Accounts // 4. Get All Bank Accounts
server.get( server.get(`${API_PREFIX}/`, async (request, reply): Promise<BankAccount[]> => {
`${API_PREFIX}/`, // No input validation needed for getting all items usually
async (request, reply): Promise<BankAccount[]> => { try {
// No input validation needed for getting all items usually const accounts = await prisma.bankAccount.findMany({
try { orderBy: { createdAt: 'desc' },
const accounts = await prisma.bankAccount.findMany({ });
orderBy: { createdAt: 'desc' } return accounts;
}); } catch (error: any) {
return accounts; server.log.error(error);
} catch (error: any) { reply.code(500);
server.log.error(error); throw new Error('Failed to retrieve bank accounts.');
reply.code(500); }
throw new Error('Failed to retrieve bank accounts.'); });
}
}
);
// Optional: Get Single Bank Account by ID // Optional: Get Single Bank Account by ID
server.get( server.get(
`${API_PREFIX}/:id`, `${API_PREFIX}/:id`,
{ {
schema: { // Define Zod schema for params schema: {
params: paramsSchema // Define Zod schema for params
} params: paramsSchema,
}, },
async (request, reply): Promise<BankAccount> => { },
try { async (request, reply): Promise<BankAccount> => {
// request.params.id is now a validated number try {
const { id } = request.params; // request.params.id is now a validated number
const { id } = request.params;
const account = await prisma.bankAccount.findUnique({ const account = await prisma.bankAccount.findUnique({
where: { id: id }, // Use the validated numeric ID where: { id: id }, // Use the validated numeric ID
}); });
if (!account) { if (!account) {
reply.code(404); reply.code(404);
throw new Error(`Bank account with ID ${id} not found.`); throw new Error(`Bank account with ID ${id} not found.`);
} }
return account; return account;
} catch (error: any) { } catch (error: any) {
// Handle Zod validation errors (though should be caught by Fastify earlier) // Handle Zod validation errors (though should be caught by Fastify earlier)
if (error instanceof z.ZodError) { if (error instanceof z.ZodError) {
reply.code(400); reply.code(400);
throw new Error(`Validation Error: ${error.errors.map(e => e.message).join(', ')}`); throw new Error(`Validation Error: ${error.errors.map((e) => e.message).join(', ')}`);
} }
// If Prisma throws or other errors occur after validation // If Prisma throws or other errors occur after validation
if (!reply.sent) { if (!reply.sent) {
// Specific check for Prisma's RecordNotFound (though findUnique returns null, not throws P2025 by default) // Specific check for Prisma's RecordNotFound (though findUnique returns null, not throws P2025 by default)
// The !account check above handles the "not found" case for findUnique // The !account check above handles the "not found" case for findUnique
server.log.error(error); // Log other unexpected errors server.log.error(error); // Log other unexpected errors
reply.code(500); reply.code(500);
throw new Error('Failed to retrieve bank account.'); throw new Error('Failed to retrieve bank account.');
} }
// If reply already sent (e.g., 404), just rethrow the original error // If reply already sent (e.g., 404), just rethrow the original error
throw error; throw error;
}
} }
},
); );
// --- Graceful Shutdown --- // --- Graceful Shutdown ---
const gracefulShutdown = async (signal: string) => { const gracefulShutdown = async (signal: string) => {
console.log(`*^! Received signal ${signal}. Shutting down...`); console.log(`*^! Received signal ${signal}. Shutting down...`);
try { try {
await server.close(); await server.close();
console.log('Fastify server closed.'); console.log('Fastify server closed.');
await prisma.$disconnect(); await prisma.$disconnect();
console.log('Prisma client disconnected.'); console.log('Prisma client disconnected.');
process.exit(0); process.exit(0);
} catch (err) { } catch (err) {
console.error('Error during shutdown:', err); console.error('Error during shutdown:', err);
process.exit(1); process.exit(1);
} }
}; };
process.on('SIGINT', () => gracefulShutdown('SIGINT')); process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
// --- Start Server (unchanged) --- // --- Start Server (unchanged) ---
const start = async () => { const start = async () => {
try { try {
const host = process.env.API_HOST || '0.0.0.0'; const host = process.env.API_HOST || '0.0.0.0';
const port = parseInt(process.env.API_PORT || '3000', 10); const port = Number.parseInt(process.env.API_PORT || '3000', 10);
await server.listen({ port, host }); await server.listen({ port, host });
} catch (err) { } catch (err) {
server.log.error(err); server.log.error(err);
await prisma.$disconnect(); await prisma.$disconnect();
process.exit(1); process.exit(1);
} }
}; };
start(); start();

View File

@@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest'; import { describe, expect, it } from 'vitest';
import { GET as listAccounts } from '../pages/api/accounts/index';
import { GET as getAccount } from '../pages/api/accounts/[id]/index'; import { GET as getAccount } from '../pages/api/accounts/[id]/index';
import { GET as listTransactions } from '../pages/api/accounts/[id]/transactions/index'; import { GET as listTransactions } from '../pages/api/accounts/[id]/transactions/index';
import { GET as listAccounts } from '../pages/api/accounts/index';
import { createMockAPIContext } from './setup'; import { createMockAPIContext } from './setup';
describe('Accounts API', () => { describe('Accounts API', () => {
@@ -48,7 +48,7 @@ describe('Accounts API', () => {
it('should return empty array for account with no transactions', async () => { it('should return empty array for account with no transactions', async () => {
const response = await listTransactions( const response = await listTransactions(
createMockAPIContext({ params: { id: '999' } }) as any createMockAPIContext({ params: { id: '999' } }) as any,
); );
const transactions = await response.json(); const transactions = await response.json();

View File

@@ -1,6 +1,6 @@
import type { APIContext } from 'astro';
import { beforeEach } from 'vitest'; import { beforeEach } from 'vitest';
import { accounts, transactions } from '../data/store'; import { accounts, transactions } from '../data/store';
import type { APIContext } from 'astro';
// Create a mock APIContext factory // Create a mock APIContext factory
export function createMockAPIContext<T extends Record<string, string> = Record<string, string>>({ export function createMockAPIContext<T extends Record<string, string> = Record<string, string>>({
@@ -43,7 +43,7 @@ beforeEach(() => {
name: 'Test Savings', name: 'Test Savings',
last4: '5678', last4: '5678',
balance: 5000.0, balance: 5000.0,
} },
); );
// Reset transactions to initial state // Reset transactions to initial state
@@ -62,6 +62,6 @@ beforeEach(() => {
date: '2025-04-24', date: '2025-04-24',
description: 'Test Transaction 2', description: 'Test Transaction 2',
amount: 100.0, amount: 100.0,
} },
); );
}); });

View File

@@ -7,13 +7,13 @@
// - Add load testing for API endpoints // - Add load testing for API endpoints
// - Implement test data factories // - Implement test data factories
import { describe, it, expect } from 'vitest'; import { describe, expect, it } from 'vitest';
import { POST as createTransaction } from '../pages/api/transactions/index';
import {
PUT as updateTransaction,
DELETE as deleteTransaction,
} from '../pages/api/transactions/[id]/index';
import { accounts, transactions } from '../data/store'; import { accounts, transactions } from '../data/store';
import {
DELETE as deleteTransaction,
PUT as updateTransaction,
} from '../pages/api/transactions/[id]/index';
import { POST as createTransaction } from '../pages/api/transactions/index';
import type { Transaction } from '../types'; import type { Transaction } from '../types';
import { createMockAPIContext } from './setup'; import { createMockAPIContext } from './setup';
@@ -273,7 +273,7 @@ describe('Transactions API', () => {
const initialCount = transactions.length; const initialCount = transactions.length;
const response = await deleteTransaction( const response = await deleteTransaction(
createMockAPIContext({ params: { id: '1' } }) as any createMockAPIContext({ params: { id: '1' } }) as any,
); );
expect(response.status).toBe(204); expect(response.status).toBe(204);
@@ -292,7 +292,7 @@ describe('Transactions API', () => {
it('should return 404 for non-existent transaction', async () => { it('should return 404 for non-existent transaction', async () => {
const response = await deleteTransaction( const response = await deleteTransaction(
createMockAPIContext({ params: { id: '999' } }) as any createMockAPIContext({ params: { id: '999' } }) as any,
); );
const error = await response.json(); const error = await response.json();
@@ -313,7 +313,7 @@ describe('Transactions API', () => {
transactions.push(testTransaction); transactions.push(testTransaction);
const response = await deleteTransaction( const response = await deleteTransaction(
createMockAPIContext({ params: { id: 'test-delete' } }) as any createMockAPIContext({ params: { id: 'test-delete' } }) as any,
); );
const error = await response.json(); const error = await response.json();