Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
32e3164
recreated backend database
FedericoLeiva12 Oct 9, 2025
87b0d71
feat: add seed script to package.json for database seeding
FedericoLeiva12 Oct 9, 2025
705d96d
feat: implement account types and enhance accounts management
FedericoLeiva12 Oct 13, 2025
60718ae
docs: update backend development guide and improve code formatting
FedericoLeiva12 Oct 13, 2025
ee22919
Merge branch 'master' of https://github.com/The-Creative-Programming-…
FedericoLeiva12 Oct 13, 2025
af684b4
fix: update mock data structure for consistency
FedericoLeiva12 Oct 13, 2025
e9ba125
refactor: improve query conditions and enhance user handling
FedericoLeiva12 Oct 14, 2025
3cba21c
format correctly
FleetAdmiralJakob Oct 23, 2025
b5355d7
Merge branch 'master' into federicoleiva/fin-25-implement-backend-for…
FleetAdmiralJakob Oct 23, 2025
0bc50c4
comment the behaviour
FleetAdmiralJakob Oct 23, 2025
c66112d
improve the names of the commands in the package.json
FleetAdmiralJakob Oct 28, 2025
c4957e7
fix issues with the migrations
FleetAdmiralJakob Oct 28, 2025
200d817
fix typos
FleetAdmiralJakob Oct 28, 2025
55c80cb
fix typos
FleetAdmiralJakob Oct 28, 2025
fd9d48e
remove unnecessary optional chaining
FleetAdmiralJakob Oct 28, 2025
35bbb91
improve commenting
FleetAdmiralJakob Oct 28, 2025
2e24435
fix error handling and unused code
FleetAdmiralJakob Oct 28, 2025
ae7db7e
fix the sign-up and account creation process
FleetAdmiralJakob Oct 31, 2025
010a387
switched to using the real data on the dashboard page
FleetAdmiralJakob Oct 31, 2025
b50ea4d
make intent clear
FleetAdmiralJakob Oct 31, 2025
7a58670
Improve type safety and efficiency in account filtering
FleetAdmiralJakob Oct 31, 2025
240d40f
added some todos
FleetAdmiralJakob Oct 31, 2025
f1d8e2f
Add newline at end of file.
FleetAdmiralJakob Oct 31, 2025
ed578d5
resolved comments
FleetAdmiralJakob Nov 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .idea/sqldialects.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

150 changes: 150 additions & 0 deletions docs/backend-development.md
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to have everything in one place, and we figured out that the wiki is the best for that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A problem we haven't thought about yet then is that if we put everything in the wiki AIs won't be able to access all of that information......... hmmmm......

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean its important for like GH Copilot if it creates a PR?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not only that but also if it works in your IDE. Then it has context and knows what to do.

Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
## Backend Development Guide

This guide documents how we build backend features in this repository to ensure consistency, type-safety, and security.

### Tech Stack and Principles

- **Runtime**: Node/TypeScript with strict typing. Avoid `any`.
- **API**: tRPC routers under `src/server/routers` using `protectedProcedure`.
- **ORM**: Drizzle ORM with PostgreSQL; schema in `src/db/schema.ts`.
- **Validation**: Zod (`import { z } from "zod"`).
- **Errors**: TRPCError helpers in `src/errors/trpc.ts` and message keys in `src/errors/messages.ts`.
- **Types**: Shared API-facing types in `src/types/index.ts`.

### Directory Layout

- `src/server/routers/*`: One router per domain (e.g., `accounts.ts`, `transactions.ts`).
- `src/server/api/*`: tRPC setup (`trpc.ts`, `root.ts`).
- `src/db/*`: Drizzle DB client and schema.
- `src/errors/*`: Error helpers and message keys.
- `src/types/*`: Shared TypeScript interfaces for API responses.
- `drizzle/*`: SQL migrations and snapshot metadata.

### Authentication and Authorization

- **Always use `protectedProcedure`** for endpoints that require a logged-in user; access the user via `ctx.userId`.
- **Never trust client-provided user IDs**. Filter by `ctx.userId` on the server.
- **Ownership checks are mandatory** for any resource scoped to a user.
- Example: Before creating a `transaction` for an `account`, verify that `account.userId === ctx.userId`.
- Example: When listing transactions, always restrict results to accounts owned by the current user.

### Error Handling Conventions

- Use the error factory in `src/errors/trpc.ts` to throw typed errors (e.g., `errors.forbidden(...)`).
- **Do not hardcode strings**. Use `src/errors/messages.ts` for error message keys.
- **Message values are camelCase** and represent i18n keys, e.g., `currencyNotFound`, `accountNotOwned`.
- Prefer reusing generic keys where possible (`forbidden`, `notFound`, `badRequest`). Add new keys when necessary.

### Drizzle ORM Guidelines

- Import comparison helpers from `drizzle-orm`: `eq`, `gte`, `lte`, `and`.
- Prefer typed selects with aliases:
- Use `select({ transactions: transactionsTable, categories: categoriesTable, accounts: accountsTable })`.
- Use `innerJoin` when related rows must exist; use `leftJoin` only when nulls are expected.
- Compose filters using a single `where` expression (build an `and(...)` chain) instead of mutating the builder repeatedly.
- For conditional filtering, build a `whereExpr` (`SQL | undefined`) and apply it once: `const rows = whereExpr ? await qb.where(whereExpr).execute() : await qb.execute()`.

### Date/Time and Numeric Types

- Use `timestamp({ withTimezone: true })` in schema; pass and compare as `Date` objects in code.
- Use `gte`/`lte` with `Date` values (not milliseconds). Example: `gte(transactionsTable.datetime, input.startDate)`.
- For currency amounts, use Drizzle `decimal({ precision, scale, mode: "string" })` and handle values as strings at the API boundary to avoid floating-point issues.

### Zod Validation Patterns

- Import with `import { z } from "zod"`.
- Validate timestamps as `z.date()`.
- Validate amounts as `z.string()` (paired with `decimal(..., mode: "string")`).
- Optional fields use `.optional()`; keep inputs minimal and explicit.

### Types and Response Shapes

- Use `src/types/index.ts` interfaces for API responses (e.g., `AccountWithCurrencyAndType`, `TransactionWithCategoryAndAccount`).
- Build responses by mapping typed Drizzle rows to these interfaces. Avoid returning raw Drizzle shapes if they do not match the shared types.
- Avoid `any`, `unknown`, and explicit `PgSelectBase` casts. Let Drizzle infer types via `select({ ... })` with aliases.

### Ownership Enforcement Patterns

- Create operations (example: transactions):
- First, verify ownership of the parent resource via Drizzle `select` on the owner table (e.g., `accountsTable.userId === ctx.userId`).
- If not owned, throw `errors.forbidden(error_messages.accountNotOwned)`.
- Read/List operations:
- Scope always results by joining the owner table first and applying `where(eq(ownerTable.userId, ctx.userId))`.
- If the client passes an ID filter (e.g., `accountId`), optionally pre-validate ownership and throw `forbidden` if not owned.

### Example: Secure and Typed List Query

```ts
import type { SQL } from "drizzle-orm";
const conditions: (SQL | undefined)[] = [
eq(accountsTable.userId, ctx.userId!),
input.accountId
? eq(transactionsTable.accountId, input.accountId)
: undefined,
input.categoryId
? eq(transactionsTable.categoryId, input.categoryId)
: undefined,
input.startDate
? gte(transactionsTable.datetime, input.startDate)
: undefined,
input.endDate ? lte(transactionsTable.datetime, input.endDate) : undefined,
];
const typed = conditions.filter((c): c is SQL => Boolean(c));
const whereExpr = typed.length ? and(...typed) : undefined;

const qb = db
.select({
transactions: transactionsTable,
categories: categoriesTable,
accounts: accountsTable,
})
.from(transactionsTable)
.innerJoin(accountsTable, eq(transactionsTable.accountId, accountsTable.id))
.innerJoin(
categoriesTable,
eq(transactionsTable.categoryId, categoriesTable.id),
);

const rows = whereExpr
? await qb.where(whereExpr).execute()
: await qb.execute();
return rows.map((row) => ({
...row.transactions,
category: row.categories,
account: row.accounts,
}));
```

### Error Messages (i18n-ready)

- Add new keys to `src/errors/messages.ts` with camelCase values, e.g.:
- `currencyNotFound`, `accountTypeNotFound`, `accountNotOwned`, `transactionNotFound`.
- UI will translate these keys later via i18n; backend should only emit the keys.

### Router Conventions

- Name operations clearly: `addX` for create, `getX` for list, `updateX`, `deleteX` if needed.
- Keep each router focused on a single domain, and register it in `src/server/api/root.ts`.
- Inputs: Zod schemas colocated in the procedure.
- Outputs: Typed return values using shared interfaces.

### Migrations and Seeding

- Manage schema changes with Drizzle migrations in `drizzle/`.
- Keep `src/scripts/seed.ts` aligned with current schema and business rules (ownership, required fields).
- Prefer deterministic seeds for development.

### Linting and Quality

- No `any` in production code. Let types flow from Drizzle and Zod.
- Keep functions small and focused. Use early returns.
- Avoid catching errors unless adding meaningful handling or mapping to typed TRPC errors.

### Adding a New Feature (Checklist)

- Define/extend schema in `src/db/schema.ts` and generate a migration.
- Add router or extend existing one under `src/server/routers`.
- Validate inputs with Zod; enforce ownership with Drizzle where applicable.
- Use `errors` + `error_messages` for failures (camelCase message keys).
- Return typed responses using `src/types/index.ts`.
- Add seeds/tests if relevant.
11 changes: 0 additions & 11 deletions drizzle/0000_mute_thunderbolt_ross.sql

This file was deleted.

49 changes: 49 additions & 0 deletions drizzle/0000_unique_mercury.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
CREATE TABLE "accounts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"userId" varchar(255) NOT NULL,
"bankName" varchar(255) NOT NULL,
"currentBalance" numeric(18, 2) NOT NULL,
"reference" varchar(255) NOT NULL,
"usage" varchar(255) NOT NULL,
"currencyId" uuid NOT NULL,
"createdAt" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "categories" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" varchar(100) NOT NULL,
"slug" varchar(120) NOT NULL
);
--> statement-breakpoint
CREATE TABLE "currencies" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" varchar(100) NOT NULL,
"code" varchar(10) NOT NULL
);
--> statement-breakpoint
CREATE TABLE "transactions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"accountId" uuid NOT NULL,
"categoryId" uuid NOT NULL,
"company" varchar(255) NOT NULL,
"amount" numeric(18, 2) NOT NULL,
"datetime" timestamp with time zone NOT NULL,
"description" text NOT NULL,
"createdAt" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "users" (
"id" varchar(255) PRIMARY KEY NOT NULL
);
--> statement-breakpoint
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_userId_users_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_currencyId_currencies_id_fk" FOREIGN KEY ("currencyId") REFERENCES "public"."currencies"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_accountId_accounts_id_fk" FOREIGN KEY ("accountId") REFERENCES "public"."accounts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "transactions" ADD CONSTRAINT "transactions_categoryId_categories_id_fk" FOREIGN KEY ("categoryId") REFERENCES "public"."categories"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "accounts_user_idx" ON "accounts" USING btree ("userId");--> statement-breakpoint
CREATE INDEX "accounts_currency_idx" ON "accounts" USING btree ("currencyId");--> statement-breakpoint
CREATE UNIQUE INDEX "categories_slug_unique_idx" ON "categories" USING btree ("slug");--> statement-breakpoint
CREATE UNIQUE INDEX "currencies_code_unique_idx" ON "currencies" USING btree ("code");--> statement-breakpoint
CREATE INDEX "transactions_account_idx" ON "transactions" USING btree ("accountId");--> statement-breakpoint
CREATE INDEX "transactions_category_idx" ON "transactions" USING btree ("categoryId");--> statement-breakpoint
CREATE INDEX "transactions_datetime_idx" ON "transactions" USING btree ("datetime");
41 changes: 41 additions & 0 deletions drizzle/0001_nostalgic_master_mold.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
CREATE EXTENSION IF NOT EXISTS pgcrypto;

CREATE TABLE "account_types" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" varchar(100) NOT NULL,
"slug" varchar(120) NOT NULL
);
--> statement-breakpoint

-- 1) Add column nullable
ALTER TABLE "accounts" ADD COLUMN "typeId" uuid;--> statement-breakpoint

-- 2) (Optional but recommended) Ensure a default account type exists for backfilling
INSERT INTO "account_types" ("name", "slug")
SELECT 'Private', 'private'
ON CONFLICT ("slug") DO NOTHING;--> statement-breakpoint

-- 3) Backfill existing rows (choose the appropriate default type)
UPDATE "accounts" a
SET "typeId" = at.id
FROM "account_types" at
WHERE a."typeId" IS NULL
AND at."slug" = 'private';--> statement-breakpoint

-- 4) Enforce NOT NULL
ALTER TABLE "accounts" ALTER COLUMN "typeId" SET NOT NULL;--> statement-breakpoint

-- 5) Keep the unique index on account_types.slug
CREATE UNIQUE INDEX "account_types_slug_unique_idx"
ON "account_types" USING btree ("slug");--> statement-breakpoint

-- 6) Add the foreign key constraint
ALTER TABLE "accounts"
ADD CONSTRAINT "accounts_typeId_account_types_id_fk"
FOREIGN KEY ("typeId")
REFERENCES "public"."account_types"("id")
ON DELETE RESTRICT
ON UPDATE NO ACTION;--> statement-breakpoint

-- 7) Add an index on accounts.typeId for performance
CREATE INDEX "accounts_type_idx" ON "accounts" USING btree ("typeId");
12 changes: 0 additions & 12 deletions drizzle/0001_tense_satana.sql

This file was deleted.

7 changes: 7 additions & 0 deletions drizzle/0002_careless_gladiator.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TYPE "public"."references" AS ENUM('private', 'business', 'savings', 'shared');--> statement-breakpoint
ALTER TABLE "account_types" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
DROP TABLE "account_types" CASCADE;--> statement-breakpoint
--> statement-breakpoint
DROP INDEX "accounts_type_idx";--> statement-breakpoint
ALTER TABLE "accounts" ALTER COLUMN "reference" SET DATA TYPE "public"."references" USING "reference"::"public"."references";--> statement-breakpoint
ALTER TABLE "accounts" DROP COLUMN "typeId";
2 changes: 0 additions & 2 deletions drizzle/0002_icy_madrox.sql

This file was deleted.

Loading