-
Notifications
You must be signed in to change notification settings - Fork 1
FIN-25: Implement backend for existing screens #69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
FedericoLeiva12
wants to merge
24
commits into
master
Choose a base branch
from
federicoleiva/fin-25-implement-backend-for-existing-screens
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
32e3164
recreated backend database
FedericoLeiva12 87b0d71
feat: add seed script to package.json for database seeding
FedericoLeiva12 705d96d
feat: implement account types and enhance accounts management
FedericoLeiva12 60718ae
docs: update backend development guide and improve code formatting
FedericoLeiva12 ee22919
Merge branch 'master' of https://github.com/The-Creative-Programming-…
FedericoLeiva12 af684b4
fix: update mock data structure for consistency
FedericoLeiva12 e9ba125
refactor: improve query conditions and enhance user handling
FedericoLeiva12 3cba21c
format correctly
FleetAdmiralJakob b5355d7
Merge branch 'master' into federicoleiva/fin-25-implement-backend-for…
FleetAdmiralJakob 0bc50c4
comment the behaviour
FleetAdmiralJakob c66112d
improve the names of the commands in the package.json
FleetAdmiralJakob c4957e7
fix issues with the migrations
FleetAdmiralJakob 200d817
fix typos
FleetAdmiralJakob 55c80cb
fix typos
FleetAdmiralJakob fd9d48e
remove unnecessary optional chaining
FleetAdmiralJakob 35bbb91
improve commenting
FleetAdmiralJakob 2e24435
fix error handling and unused code
FleetAdmiralJakob ae7db7e
fix the sign-up and account creation process
FleetAdmiralJakob 010a387
switched to using the real data on the dashboard page
FleetAdmiralJakob b50ea4d
make intent clear
FleetAdmiralJakob 7a58670
Improve type safety and efficiency in account filtering
FleetAdmiralJakob 240d40f
added some todos
FleetAdmiralJakob f1d8e2f
Add newline at end of file.
FleetAdmiralJakob ed578d5
resolved comments
FleetAdmiralJakob File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"); |
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"; |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove this read me and add it to our wiki: https://github.com/[The-Creative-Programming-Group/finance-io](https://github.com/The-Creative-Programming-Group/finance-io/wiki)/wiki
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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......
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.