Reccurring transactions#9
Conversation
… to accept users credentials compulsorily
- Introduced RecurringTransactionsModule, Controller, Service, and Repository - Implemented Create and Update DTOs for recurring transactions - Enhanced schema with recurring transactions table and relationships - Updated TransactionsRepository for soft delete functionality - Added recurrence frequency and status enums to transaction types
There was a problem hiding this comment.
Pull request overview
This PR adds recurring-transactions support to the NestJS + Drizzle expense tracker, alongside introducing JWT-based authentication/authorization and enhancing the DB schema to support recurrence and soft-deleted transactions.
Changes:
- Added a full
RecurringTransactionsfeature set (module/controller/service/repository + DTOs) and recurrence enums. - Introduced JWT auth (strategy + guard), plus role-based access control for admin-only endpoints.
- Updated DB schema for recurring transactions + transaction soft-delete (and updated repository/service/controller logic accordingly).
Reviewed changes
Copilot reviewed 39 out of 40 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/return-first.ts | Adds a small helper to safely return the first row from a query result. |
| src/users/users.service.ts | Implements user creation/listing/lookup and admin checks (bcrypt hashing). |
| src/users/users.service.spec.ts | Adds basic UsersService test scaffold. |
| src/users/users.repository.ts | Adds users persistence methods using Drizzle. |
| src/users/users.module.ts | Registers users providers/controllers (and exports repository). |
| src/users/users.controller.ts | Adds users endpoints with JWT + admin-only role guarding for reads. |
| src/users/users.controller.spec.ts | Adds basic UsersController test scaffold. |
| src/users/roles.guard.ts | Introduces role-based authorization guard. |
| src/users/roles.decorator.ts | Adds @Roles() decorator for role metadata. |
| src/users/dto/users-dto.ts | Adds user DTOs including password validation. |
| src/transactions/transactions.types.ts | Adds recurrence frequency/status enums. |
| src/transactions/transactions.service.ts | Enforces per-user access checks and switches to soft delete. |
| src/transactions/transactions.service.spec.ts | Updates TransactionsService test scaffold with repository mock. |
| src/transactions/transactions.repository.ts | Adds soft-delete behavior + “find by id and user” query. |
| src/transactions/transactions.controller.ts | Secures endpoints with JWT; admin-only list-all; user-scoped CRUD. |
| src/transactions/transactions.controller.spec.ts | Adds TransactionsController test scaffold with service mock. |
| src/transactions/dto/create-transaction.dto.ts | Minor DTO import/validator adjustments. |
| src/recurring-transactions/recurring-transactions.service.ts | Implements recurring transaction scheduling validation and CRUD logic. |
| src/recurring-transactions/recurring-transactions.repository.ts | Adds recurring transactions persistence methods. |
| src/recurring-transactions/recurring-transactions.module.ts | Registers recurring transactions providers/controllers. |
| src/recurring-transactions/recurring-transactions.controller.ts | Adds JWT-protected recurring-transactions CRUD endpoints. |
| src/recurring-transactions/dto/update-recurring-transaction.dto.ts | Adds update DTO via PartialType. |
| src/recurring-transactions/dto/create-recurring-transaction.dto.ts | Adds create DTO with recurrence and schedule validation decorators. |
| src/db/schema.ts | Adds recurring_transactions table, relationships, indexes, and deletedAt for transactions. |
| src/config/env.ts | Adds JWT_SECRET to env config surface. |
| src/auth/strategies/jwt.strategy/jwt.strategy.ts | Adds Passport JWT strategy for request authentication. |
| src/auth/strategies/jwt.strategy/jwt.strategy.spec.ts | Adds JwtStrategy test (currently problematic due to env requirements). |
| src/auth/interfaces/authenticated-request.interface.ts | Adds a typed Express request with authenticated user shape. |
| src/auth/guards/jwt-auth/jwt-auth.guard.ts | Adds JWT auth guard wrapper. |
| src/auth/guards/jwt-auth/jwt-auth.guard.spec.ts | Adds basic JwtAuthGuard test scaffold. |
| src/auth/dto/users.dto.ts | Adds an auth-focused user DTO for login response. |
| src/auth/dto/login.dto.ts | Adds login DTO with validation. |
| src/auth/auth.service.ts | Implements login with bcrypt + JWT issuance. |
| src/auth/auth.service.spec.ts | Adds basic AuthService test scaffold. |
| src/auth/auth.module.ts | Registers auth module providers/controllers. |
| src/auth/auth.controller.ts | Adds login + JWT-protected profile endpoints. |
| src/auth/auth.controller.spec.ts | Adds basic AuthController test scaffold. |
| src/app.module.ts | Wires in AuthModule and RecurringTransactionsModule. |
| package.json | Adds passport/JWT/bcrypt deps and Jest moduleNameMapper for src/* imports. |
| package-lock.json | Locks added dependencies. |
Comments suppressed due to low confidence (1)
src/transactions/transactions.repository.ts:12
UpdateTransactionInputomitscreatedOnandlastModifiedOn, but the schema usescreatedAt/updatedAt(and now also hastransactionDate/deletedAt). As written, the update type will still allow updating system-managed timestamps, and the omit list references non-existent fields. Update the omit list to match the actual schema fields you want to prevent updating (e.g.,createdAt,updatedAt,deletedAt,transactionDate, etc.).
type UpdateTransactionInput = Partial<Omit<
CreateTransactionInput,
'userId' | 'id' | 'createdOn' | 'lastModifiedOn'>
>;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async softDelete(id: number) { | ||
| return this.dbClient | ||
| .delete(transactions) | ||
| .update(transactions) | ||
| .set({ deletedAt: sql`CURRENT_TIMESTAMP` }) | ||
| .where(eq(transactions.id, id)); |
There was a problem hiding this comment.
softDelete() only filters by transaction id. Prefer enforcing ownership/deleted-state atomically in the update by including userId and deletedAt IS NULL in the where clause (and accept userId in this method), to prevent accidental misuse and reduce TOCTOU risks.
| id: number; | ||
| email: string; | ||
| username: string; | ||
| userType: string; | ||
|
|
There was a problem hiding this comment.
UserDto exposes username, but other user-facing DTOs in this PR use name. This inconsistent naming will confuse API consumers. Align on a single field name (preferably name) or provide a clear migration plan/documentation if both must exist.
| import { Module } from '@nestjs/common'; | ||
| import { AuthService } from './auth.service'; | ||
| import { AuthController } from './auth.controller'; | ||
| import { UsersModule } from 'src/users/users.module'; |
There was a problem hiding this comment.
This src/... absolute import will be emitted as-is in the compiled JS. With the current start:prod script (node dist/main) and no runtime path-alias loader configured, Node won't resolve src/* specifiers and the app can fail at runtime. Prefer relative imports here, or add a proper TypeScript paths mapping and register a runtime resolver for production builds.
| import { UsersModule } from 'src/users/users.module'; | |
| import { UsersModule } from '../users/users.module'; |
| it('should be defined', () => { | ||
| expect(new JwtStrategy()).toBeDefined(); |
There was a problem hiding this comment.
JwtStrategy throws in its constructor when env.JWT_SECRET is not set, but this unit test instantiates the strategy without configuring the secret first, so it will fail in most test environments. Update the test to set the secret before importing/constructing the strategy (or change the strategy to not throw in the constructor and instead validate configuration during app bootstrap).
| it('should be defined', () => { | |
| expect(new JwtStrategy()).toBeDefined(); | |
| it('should be defined', () => { | |
| const originalSecret = process.env.JWT_SECRET; | |
| process.env.JWT_SECRET = process.env.JWT_SECRET || 'test-secret'; | |
| expect(new JwtStrategy()).toBeDefined(); | |
| process.env.JWT_SECRET = originalSecret; |
| {message: 'Password must be at least 8 characters long and include uppercase letters, lowercase letters, numbers, and symbols.', | ||
| } | ||
| ) | ||
| passwordHash: string; |
There was a problem hiding this comment.
This field is named passwordHash, but the service hashes it, which implies the client is sending a plain-text password. This is misleading for API consumers and increases the risk of accidental double-hashing. Consider renaming the DTO field to password (or similar) and mapping internally; if breaking changes are a concern, accept both names temporarily.
| getProfile(@Request() req: ExpressRequest) { | ||
| return (req as any).user; | ||
| } |
There was a problem hiding this comment.
This endpoint returns (req as any).user even though the PR introduces a typed AuthenticatedRequest interface. Use @Request() req: AuthenticatedRequest (or @Req() with the same type) to avoid any casts and keep the request user shape consistent across controllers.
| const updatePayload: Record<string, unknown> = { ...dto }; | ||
|
|
||
| if (dto.startAt !== undefined) { | ||
| updatePayload.startAt = this.toDate(dto.startAt, 'startAt'); | ||
| } |
There was a problem hiding this comment.
updatePayload is typed as Record<string, unknown>, but the repository update method expects a typed partial of the insert model; this will fail TypeScript assignment because unknown isn't assignable to the expected field types. Type updatePayload to match Parameters<RecurringTransactionsRepository['updateByIdAndUserId']>[2] (or export a dedicated update input type) before passing it to the repository.
| import { Module } from '@nestjs/common'; | ||
| import { UsersService } from './users.service'; | ||
| import { DbModule } from 'src/db/db.module'; | ||
| import { UsersController } from './users.controller'; |
There was a problem hiding this comment.
This module uses a src/... absolute import for DbModule, while other modules in the repo (e.g., src/transactions/transactions.module.ts) use relative imports for internal modules. Mixing import styles makes refactors and tooling configuration harder; please standardize on one approach across modules (either relative imports everywhere or src/ aliases everywhere).
| .notNull() | ||
| .$onUpdate(() => sql`CURRENT_TIMESTAMP`), | ||
|
|
||
| amount: decimal('amount', { precision: 15, scale: 3, mode: 'number' }), |
There was a problem hiding this comment.
transactions.amount is nullable in the schema, but CreateTransactionDto requires amount (and the service always inserts it). If amount should always be present, mark the column as .notNull() to keep DB constraints aligned with API validation.
| amount: decimal('amount', { precision: 15, scale: 3, mode: 'number' }), | |
| amount: decimal('amount', { precision: 15, scale: 3, mode: 'number' }).notNull(), |
##What I did
Introduced RecurringTransactionsModule, Controller, Service, and Repository