From 97e6c3dfd2bbcfdbb87db03d9d19a24170d3110a Mon Sep 17 00:00:00 2001 From: dmakarau Date: Fri, 3 Oct 2025 21:08:43 +0200 Subject: [PATCH 1/8] Implemented the migration for table habits_categories --- CLAUDE.md | 74 ------------------- .../CreateHabitsCategoryTableMigration.swift | 27 +++++++ Sources/HabitTrackerAppServer/configure.swift | 1 + 3 files changed, 28 insertions(+), 74 deletions(-) delete mode 100644 CLAUDE.md create mode 100644 Sources/HabitTrackerAppServer/Migrations/CreateHabitsCategoryTableMigration.swift diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 398c0a4..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,74 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -HabitTracker API Server is a Swift-based REST API server built with Vapor 4 framework for habit tracking functionality. This is a learning project focused on backend development with Vapor. - -## Technology Stack - -- **Language**: Swift 6.0+ -- **Framework**: Vapor 4.115.0+ -- **Platform**: macOS 13+ -- **Database**: PostgreSQL (production), SQLite (development) -- **Authentication**: JWT -- **Testing**: Swift Testing framework -- **Deployment**: Docker, Heroku support - -## Development Commands - -### Building and Running -```bash -# Resolve dependencies -swift package resolve - -# Run the server in development mode -swift run HabitTrackerAppServer serve --hostname 0.0.0.0 --port 8080 - -# Run tests -swift test - -# Docker commands -docker compose build -docker compose up app -docker compose down -``` - -## Architecture - -The project follows a standard Vapor application structure: - -### Core Files -- `entrypoint.swift` - Application entry point using async/await pattern -- `configure.swift` - Application configuration and middleware setup -- `routes.swift` - Route definitions (currently contains basic hello world routes) - -### Current State -- Basic Vapor server setup is complete -- Simple routes implemented for testing -- Test infrastructure in place using Swift Testing framework -- Docker configuration ready for containerized deployment - -### Planned Architecture (per README) -- JWT authentication system -- RESTful API endpoints for: - - User registration/login/logout - - Categories CRUD - - Habits CRUD - - Habit entries and calendar data -- Protected routes with JWT middleware - -## Environment Configuration - -The application expects a `.env` file with: -- `DATABASE_URL` - Database connection string -- `JWT_SECRET` - JWT signing secret -- `LOG_LEVEL` - Logging level (optional, defaults to debug) - -## Development Notes - -- Project uses Swift 6.0 with ExistentialAny upcoming feature enabled -- Uses VaporTesting for HTTP endpoint testing -- Currently in early development stage - basic server infrastructure is set up but main API features are not yet implemented -- Code follows standard Vapor conventions and patterns \ No newline at end of file diff --git a/Sources/HabitTrackerAppServer/Migrations/CreateHabitsCategoryTableMigration.swift b/Sources/HabitTrackerAppServer/Migrations/CreateHabitsCategoryTableMigration.swift new file mode 100644 index 0000000..d624284 --- /dev/null +++ b/Sources/HabitTrackerAppServer/Migrations/CreateHabitsCategoryTableMigration.swift @@ -0,0 +1,27 @@ +// +// CreateHabitsCategoryTableMigration.swift +// HabitTrackerAppServer +// +// Created by Denis Makarau on 03.10.25. +// + +import Foundation +import Fluent + +struct CreateHabitsCategoryTableMigration: AsyncMigration { + + func prepare(on database: any Database) async throws { + try await database.schema("habits_categories") + .id() + .field("name", .string, .required) + .field("color_code", .string, .required) + .field("user_id", .uuid, .required, .references("users", "id")) + .create() + } + + func revert(on database: any Database) async throws { + try await database.schema("habits_categories") + .delete() + } +} + diff --git a/Sources/HabitTrackerAppServer/configure.swift b/Sources/HabitTrackerAppServer/configure.swift index 2e8f105..5c51c2b 100644 --- a/Sources/HabitTrackerAppServer/configure.swift +++ b/Sources/HabitTrackerAppServer/configure.swift @@ -30,6 +30,7 @@ public func configure(_ app: Application) async throws { } app.migrations.add(CreateUsersTableMigration()) + app.migrations.add(CreateHabitsCategoryTableMigration()) // Auto-migrate in testing environment (creates tables automatically) if app.environment == .testing { From 655aa6344df79a11569832ae14c57a44ae3d98a5 Mon Sep 17 00:00:00 2001 From: dmakarau Date: Fri, 3 Oct 2025 22:01:25 +0200 Subject: [PATCH 2/8] Added Categroy model --- .../Models/Category.swift | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 Sources/HabitTrackerAppServer/Models/Category.swift diff --git a/Sources/HabitTrackerAppServer/Models/Category.swift b/Sources/HabitTrackerAppServer/Models/Category.swift new file mode 100644 index 0000000..bc804e0 --- /dev/null +++ b/Sources/HabitTrackerAppServer/Models/Category.swift @@ -0,0 +1,49 @@ +// Category.swift +// HabitTrackerAppServer +// +// Created by Denis Makarau on 03.10.25. +// + +import Foundation +import Fluent +import Vapor + +final class Category: Model, Validatable, Content, @unchecked Sendable { + static let schema = "habits_categories" + + @ID(key: .id) + var id: UUID? + + @Field(key: "name") + var name: String + + @Field(key: "color_code") + var colorCode: String + + @Parent(key: "user_id") + var user: User + + init() {} + + init(id: UUID? = nil, name: String, colorCode: String, userId: UUID) { + self.id = id + self.name = name + self.colorCode = colorCode + self.$user.id = userId + } + + static func validations(_ validations: inout Validations) { + validations.add("name", as: String.self, is: !.empty, customFailureDescription: "Category name cannot be empty") + validations.add("colorCode", as: String.self, is: !.empty, customFailureDescription: "Color coe cannot be empty") + validations.add("color_code", as: String.self, is: .pattern(#"^#([A-Fa-f0-9]{6})$"#), customFailureDescription: "Color code should be in format #RRGGBB") + } + + + + + + + + +} + From d3ca55f3665dbb55fd58ac25080851e15ef7f5fa Mon Sep 17 00:00:00 2001 From: dmakarau Date: Mon, 6 Oct 2025 06:34:31 +0200 Subject: [PATCH 3/8] Added HabitsController --- Package.resolved | 2 +- .../Controllers/HabitsController.swift | 48 +++++++++++++++++++ ...HabitsCategoryResponseDTO+Extensions.swift | 22 +++++++++ Sources/HabitTrackerAppServer/configure.swift | 1 + 4 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 Sources/HabitTrackerAppServer/Controllers/HabitsController.swift create mode 100644 Sources/HabitTrackerAppServer/Extensions/HabitsCategoryResponseDTO+Extensions.swift diff --git a/Package.resolved b/Package.resolved index d45f9b1..4a2e0bc 100644 --- a/Package.resolved +++ b/Package.resolved @@ -70,7 +70,7 @@ "location" : "https://github.com/dmakarau/HabitTrackerAppSharedDTO.git", "state" : { "branch" : "main", - "revision" : "342c7c33c0e9fc30747dbc1d100939dbed3118cd" + "revision" : "f93b5fbb5d9435537ca68d80040a78478634bf0a" } }, { diff --git a/Sources/HabitTrackerAppServer/Controllers/HabitsController.swift b/Sources/HabitTrackerAppServer/Controllers/HabitsController.swift new file mode 100644 index 0000000..afe7031 --- /dev/null +++ b/Sources/HabitTrackerAppServer/Controllers/HabitsController.swift @@ -0,0 +1,48 @@ +// +// HabitsController.swift +// HabitTrackerAppServer +// +// Created by Denis Makarau on 03.10.25. +// + +import Foundation +import Vapor +import HabitTrackerAppSharedDTO + +struct HabitsController: RouteCollection { + func boot(routes: any RoutesBuilder) throws { + + // /api/:userId + let api = routes.grouped("api", ":userId") + + // POST: saving a habbit category + // /api/:userId/categories + api.post("categories", use: saveHabitCategory) + } + + @Sendable func saveHabitCategory(req: Request) async throws -> HabitsCategoryResponseDTO { + + // get the user id + guard let userId = req.parameters.get("userId", as: UUID.self) else { + throw Abort(.badRequest, reason: "Missing or invalid userId parameter") + } + // DTO for the request + let habitsCategoryRequestDTO = try req.content.decode(HabitsCategoryRequestDTO.self) + + let habitCategory = Category( + name: habitsCategoryRequestDTO.name, + colorCode: habitsCategoryRequestDTO.colorCode, + userId: userId + ) + try await habitCategory.save(on: req.db) + + // DTO for thre response + + guard let categoryResponseDTO = HabitsCategoryResponseDTO(habitCategory) else { + throw Abort(.internalServerError, reason: "Failed to create response DTO") + } + + return categoryResponseDTO + } + +} diff --git a/Sources/HabitTrackerAppServer/Extensions/HabitsCategoryResponseDTO+Extensions.swift b/Sources/HabitTrackerAppServer/Extensions/HabitsCategoryResponseDTO+Extensions.swift new file mode 100644 index 0000000..87e194b --- /dev/null +++ b/Sources/HabitTrackerAppServer/Extensions/HabitsCategoryResponseDTO+Extensions.swift @@ -0,0 +1,22 @@ +// +// HabitsCategoryResponseDTO+Extensions.swift +// HabitTrackerAppServer +// +// Created by Denis Makarau on 06.10.25. +// + +import Foundation +import HabitTrackerAppSharedDTO +import Vapor + +extension HabitsCategoryResponseDTO: @retroactive RequestDecodable {} +extension HabitsCategoryResponseDTO: @retroactive ResponseEncodable {} +extension HabitsCategoryResponseDTO: @retroactive AsyncRequestDecodable {} +extension HabitsCategoryResponseDTO: @retroactive AsyncResponseEncodable {} +extension HabitsCategoryResponseDTO: @retroactive Content { + init?(_ category: Category) { + guard let id = category.id else { return nil } + + self.init(id: id, name: category.name, colorCode: category.colorCode) + } +} diff --git a/Sources/HabitTrackerAppServer/configure.swift b/Sources/HabitTrackerAppServer/configure.swift index 5c51c2b..27b1ab7 100644 --- a/Sources/HabitTrackerAppServer/configure.swift +++ b/Sources/HabitTrackerAppServer/configure.swift @@ -38,6 +38,7 @@ public func configure(_ app: Application) async throws { } try app.register(collection: UserController()) + try app.register(collection: HabitsController()) // JWT configuration: use environment variable or default for testing From 7378c435e44aa2cce2424a9376c6515bb370e91e Mon Sep 17 00:00:00 2001 From: dmakarau Date: Mon, 6 Oct 2025 16:33:08 +0200 Subject: [PATCH 4/8] - Created API for saving category --- .../Controllers/HabitsController.swift | 19 ++++-- .../HabitTrackerAppServerLoginTests.swift | 1 + ...bitTrackerAppServerRegistrationTests.swift | 1 + ...tTrackerAppServerSavingCategoryTests.swift | 58 +++++++++++++++++++ 4 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 Tests/HabitTrackerAppServerTests/HabitTrackerAppServerSavingCategoryTests.swift diff --git a/Sources/HabitTrackerAppServer/Controllers/HabitsController.swift b/Sources/HabitTrackerAppServer/Controllers/HabitsController.swift index afe7031..7339703 100644 --- a/Sources/HabitTrackerAppServer/Controllers/HabitsController.swift +++ b/Sources/HabitTrackerAppServer/Controllers/HabitsController.swift @@ -26,18 +26,27 @@ struct HabitsController: RouteCollection { guard let userId = req.parameters.get("userId", as: UUID.self) else { throw Abort(.badRequest, reason: "Missing or invalid userId parameter") } - // DTO for the request - let habitsCategoryRequestDTO = try req.content.decode(HabitsCategoryRequestDTO.self) + + // Decode request as a simple dictionary to get name and colorCode + let requestData = try req.content.decode([String: String].self) + guard let name = requestData["name"], + let colorCode = requestData["colorCode"] else { + throw Abort(.badRequest, reason: "Missing required fields: name and colorCode") + } let habitCategory = Category( - name: habitsCategoryRequestDTO.name, - colorCode: habitsCategoryRequestDTO.colorCode, + name: name, + colorCode: colorCode, userId: userId ) try await habitCategory.save(on: req.db) - // DTO for thre response + // After saving, ensure the ID is assigned + guard habitCategory.id != nil else { + throw Abort(.internalServerError, reason: "Failed to get ID after saving category") + } + // DTO for the response guard let categoryResponseDTO = HabitsCategoryResponseDTO(habitCategory) else { throw Abort(.internalServerError, reason: "Failed to create response DTO") } diff --git a/Tests/HabitTrackerAppServerTests/HabitTrackerAppServerLoginTests.swift b/Tests/HabitTrackerAppServerTests/HabitTrackerAppServerLoginTests.swift index 53d3a1d..bbd0dc5 100644 --- a/Tests/HabitTrackerAppServerTests/HabitTrackerAppServerLoginTests.swift +++ b/Tests/HabitTrackerAppServerTests/HabitTrackerAppServerLoginTests.swift @@ -7,6 +7,7 @@ import Foundation @testable import HabitTrackerAppServer +import HabitTrackerAppSharedDTO import VaporTesting import Testing diff --git a/Tests/HabitTrackerAppServerTests/HabitTrackerAppServerRegistrationTests.swift b/Tests/HabitTrackerAppServerTests/HabitTrackerAppServerRegistrationTests.swift index 2a31564..1e1f1c6 100644 --- a/Tests/HabitTrackerAppServerTests/HabitTrackerAppServerRegistrationTests.swift +++ b/Tests/HabitTrackerAppServerTests/HabitTrackerAppServerRegistrationTests.swift @@ -1,5 +1,6 @@ @testable import HabitTrackerAppServer import VaporTesting +import HabitTrackerAppSharedDTO import Testing @Suite("App Registration Tests") diff --git a/Tests/HabitTrackerAppServerTests/HabitTrackerAppServerSavingCategoryTests.swift b/Tests/HabitTrackerAppServerTests/HabitTrackerAppServerSavingCategoryTests.swift new file mode 100644 index 0000000..2be1220 --- /dev/null +++ b/Tests/HabitTrackerAppServerTests/HabitTrackerAppServerSavingCategoryTests.swift @@ -0,0 +1,58 @@ +// +// HabitTrackerAppServerSavingCategoryTests.swift +// HabitTrackerAppServer +// +// Created by Denis Makarau on 06.10.25. +// + +@testable import HabitTrackerAppServer +import VaporTesting +import HabitTrackerAppSharedDTO +import Testing +import Fluent + +@Suite("Category Creation Tests") +struct HabitTrackerAppServerSavingCategoryTests { + + @Test("Category creation - Success") + func categoryCreationSuccess() async throws { + try await withApp(configure: configure) { app in + // First create a user using the same pattern as other tests + let userRequestBody = User(username: "testuser", password: "password") + try await app.testing().test(.POST, "/api/register") { req in + try req.content.encode(userRequestBody) + } afterResponse: { res in + #expect(res.status == .ok) + let response = try res.content.decode(RegisterResponseDTO.self) + #expect(response.error == false) + } + + // Retrieve the created user from the database + guard let createdUser = try await User.query(on: app.db) + .filter(\.$username == "testuser") + .first(), + let userId = createdUser.id else { + throw TestError.userCreationFailed + } + + // Create a simple request object with just name and colorCode (no id or userId) + let requestBody = [ + "name": "test category", + "colorCode": "#FFFFFF" + ] + + try await app.testing().test(.POST, "/api/\(userId.uuidString)/categories") { req in + try req.content.encode(requestBody) + } afterResponse: { res in + #expect(res.status == .ok) + let response = try res.content.decode(HabitsCategoryResponseDTO.self) + #expect(response.name == "test category") + #expect(response.colorCode == "#FFFFFF") + } + } + } +} + +enum TestError: Error { + case userCreationFailed +} From bb965d90caa55ee4ae145eefa7268269cc89696d Mon Sep 17 00:00:00 2001 From: Denis Makarau Date: Mon, 6 Oct 2025 16:53:15 +0200 Subject: [PATCH 5/8] Added test coverage --- .../Controllers/HabitsController.swift | 17 +- ...tTrackerAppServerSavingCategoryTests.swift | 188 +++++++++++++++++- 2 files changed, 199 insertions(+), 6 deletions(-) diff --git a/Sources/HabitTrackerAppServer/Controllers/HabitsController.swift b/Sources/HabitTrackerAppServer/Controllers/HabitsController.swift index 7339703..0bfb9b8 100644 --- a/Sources/HabitTrackerAppServer/Controllers/HabitsController.swift +++ b/Sources/HabitTrackerAppServer/Controllers/HabitsController.swift @@ -21,19 +21,30 @@ struct HabitsController: RouteCollection { } @Sendable func saveHabitCategory(req: Request) async throws -> HabitsCategoryResponseDTO { - + // get the user id guard let userId = req.parameters.get("userId", as: UUID.self) else { throw Abort(.badRequest, reason: "Missing or invalid userId parameter") } - + // Decode request as a simple dictionary to get name and colorCode let requestData = try req.content.decode([String: String].self) guard let name = requestData["name"], let colorCode = requestData["colorCode"] else { throw Abort(.badRequest, reason: "Missing required fields: name and colorCode") } - + + // Validate empty name + guard !name.isEmpty else { + throw Abort(.badRequest, reason: "Category name cannot be empty") + } + + // Validate color code format (#RRGGBB) + let colorCodePattern = #"^#([A-Fa-f0-9]{6})$"# + guard colorCode.range(of: colorCodePattern, options: .regularExpression) != nil else { + throw Abort(.badRequest, reason: "Color code should be in format #RRGGBB") + } + let habitCategory = Category( name: name, colorCode: colorCode, diff --git a/Tests/HabitTrackerAppServerTests/HabitTrackerAppServerSavingCategoryTests.swift b/Tests/HabitTrackerAppServerTests/HabitTrackerAppServerSavingCategoryTests.swift index 2be1220..5c3eb9a 100644 --- a/Tests/HabitTrackerAppServerTests/HabitTrackerAppServerSavingCategoryTests.swift +++ b/Tests/HabitTrackerAppServerTests/HabitTrackerAppServerSavingCategoryTests.swift @@ -26,7 +26,7 @@ struct HabitTrackerAppServerSavingCategoryTests { let response = try res.content.decode(RegisterResponseDTO.self) #expect(response.error == false) } - + // Retrieve the created user from the database guard let createdUser = try await User.query(on: app.db) .filter(\.$username == "testuser") @@ -34,13 +34,13 @@ struct HabitTrackerAppServerSavingCategoryTests { let userId = createdUser.id else { throw TestError.userCreationFailed } - + // Create a simple request object with just name and colorCode (no id or userId) let requestBody = [ "name": "test category", "colorCode": "#FFFFFF" ] - + try await app.testing().test(.POST, "/api/\(userId.uuidString)/categories") { req in try req.content.encode(requestBody) } afterResponse: { res in @@ -51,6 +51,188 @@ struct HabitTrackerAppServerSavingCategoryTests { } } } + + @Test("Category creation - Fail - Missing name") + func categoryCreationFailMissingName() async throws { + try await withApp(configure: configure) { app in + // Create a user + let userRequestBody = User(username: "testuser2", password: "password") + try await app.testing().test(.POST, "/api/register") { req in + try req.content.encode(userRequestBody) + } afterResponse: { res in + #expect(res.status == .ok) + } + + guard let createdUser = try await User.query(on: app.db) + .filter(\.$username == "testuser2") + .first(), + let userId = createdUser.id else { + throw TestError.userCreationFailed + } + + // Request body missing name + let requestBody = [ + "colorCode": "#FFFFFF" + ] + + try await app.testing().test(.POST, "/api/\(userId.uuidString)/categories") { req in + try req.content.encode(requestBody) + } afterResponse: { res in + #expect(res.status == .badRequest) + #expect(res.body.string.contains("Missing required fields")) + } + } + } + + @Test("Category creation - Fail - Missing colorCode") + func categoryCreationFailMissingColorCode() async throws { + try await withApp(configure: configure) { app in + // Create a user + let userRequestBody = User(username: "testuser3", password: "password") + try await app.testing().test(.POST, "/api/register") { req in + try req.content.encode(userRequestBody) + } afterResponse: { res in + #expect(res.status == .ok) + } + + guard let createdUser = try await User.query(on: app.db) + .filter(\.$username == "testuser3") + .first(), + let userId = createdUser.id else { + throw TestError.userCreationFailed + } + + // Request body missing colorCode + let requestBody = [ + "name": "test category" + ] + + try await app.testing().test(.POST, "/api/\(userId.uuidString)/categories") { req in + try req.content.encode(requestBody) + } afterResponse: { res in + #expect(res.status == .badRequest) + #expect(res.body.string.contains("Missing required fields")) + } + } + } + + @Test("Category creation - Fail - Invalid userId") + func categoryCreationFailInvalidUserId() async throws { + try await withApp(configure: configure) { app in + let requestBody = [ + "name": "test category", + "colorCode": "#FFFFFF" + ] + + try await app.testing().test(.POST, "/api/invalid-uuid/categories") { req in + try req.content.encode(requestBody) + } afterResponse: { res in + #expect(res.status == .badRequest) + } + } + } + + @Test("Category creation - Fail - Empty name") + func categoryCreationFailEmptyName() async throws { + try await withApp(configure: configure) { app in + // Create a user + let userRequestBody = User(username: "testuser4", password: "password") + try await app.testing().test(.POST, "/api/register") { req in + try req.content.encode(userRequestBody) + } afterResponse: { res in + #expect(res.status == .ok) + } + + guard let createdUser = try await User.query(on: app.db) + .filter(\.$username == "testuser4") + .first(), + let userId = createdUser.id else { + throw TestError.userCreationFailed + } + + // Request body with empty name + let requestBody = [ + "name": "", + "colorCode": "#FFFFFF" + ] + + try await app.testing().test(.POST, "/api/\(userId.uuidString)/categories") { req in + try req.content.encode(requestBody) + } afterResponse: { res in + #expect(res.status == .badRequest) + } + } + } + + @Test("Category creation - Fail - Invalid color code format") + func categoryCreationFailInvalidColorCode() async throws { + try await withApp(configure: configure) { app in + // Create a user + let userRequestBody = User(username: "testuser5", password: "password") + try await app.testing().test(.POST, "/api/register") { req in + try req.content.encode(userRequestBody) + } afterResponse: { res in + #expect(res.status == .ok) + } + + guard let createdUser = try await User.query(on: app.db) + .filter(\.$username == "testuser5") + .first(), + let userId = createdUser.id else { + throw TestError.userCreationFailed + } + + // Request body with invalid color code (missing #) + let requestBody = [ + "name": "test category", + "colorCode": "FFFFFF" + ] + + try await app.testing().test(.POST, "/api/\(userId.uuidString)/categories") { req in + try req.content.encode(requestBody) + } afterResponse: { res in + #expect(res.status == .badRequest) + } + } + } + + @Test("Category creation - Success - Valid color codes") + func categoryCreationSuccessVariousColors() async throws { + try await withApp(configure: configure) { app in + // Create a user + let userRequestBody = User(username: "testuser6", password: "password") + try await app.testing().test(.POST, "/api/register") { req in + try req.content.encode(userRequestBody) + } afterResponse: { res in + #expect(res.status == .ok) + } + + guard let createdUser = try await User.query(on: app.db) + .filter(\.$username == "testuser6") + .first(), + let userId = createdUser.id else { + throw TestError.userCreationFailed + } + + // Test various valid color codes + let validColors = ["#000000", "#FFFFFF", "#FF0000", "#00FF00", "#0000FF", "#abcdef", "#123456"] + + for (index, color) in validColors.enumerated() { + let requestBody = [ + "name": "category \(index)", + "colorCode": color + ] + + try await app.testing().test(.POST, "/api/\(userId.uuidString)/categories") { req in + try req.content.encode(requestBody) + } afterResponse: { res in + #expect(res.status == .ok) + let response = try res.content.decode(HabitsCategoryResponseDTO.self) + #expect(response.colorCode.uppercased() == color.uppercased()) + } + } + } + } } enum TestError: Error { From d55df544cdf6771731f344cde68425c3fbfd1ed0 Mon Sep 17 00:00:00 2001 From: Denis Makarau Date: Mon, 6 Oct 2025 16:55:15 +0200 Subject: [PATCH 6/8] Updated README --- README.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 796149c..0efa3c3 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,9 @@ A Swift-based REST API server for habit tracking, built with the Vapor web frame - `POST /api/register` - User registration ✅ - `POST /api/login` - User login ✅ +#### Categories +- `POST /api/categories` - Create new category ✅ + ### Planned Endpoints #### Authentication @@ -38,7 +41,6 @@ A Swift-based REST API server for habit tracking, built with the Vapor web frame #### Categories - `GET /api/categories` - Get all categories -- `POST /api/categories` - Create new category - `PUT /api/categories/:id` - Update category - `DELETE /api/categories/:id` - Delete category @@ -122,15 +124,19 @@ HabitTracker-API-Server/ │ ├── configure.swift # Application configuration │ ├── routes.swift # Route definitions │ ├── Controllers/ # API controllers -│ │ └── UserController.swift # User registration/auth controller +│ │ ├── UserController.swift # User registration/auth controller +│ │ └── HabitsController.swift # Habits and categories controller │ ├── Models/ # Data models │ │ ├── User.swift # User model with validation +│ │ ├── Category.swift # Category model │ │ └── AuthPayload.swift # JWT payload structure │ ├── Extensions/ # Protocol conformances for shared types │ │ ├── RegisterResponseDTO+Extensions.swift # Vapor Content conformance -│ │ └── LoginResponseDTO+Extensions.swift # Vapor Content conformance +│ │ ├── LoginResponseDTO+Extensions.swift # Vapor Content conformance +│ │ └── HabitsCategoryResponseDTO+Extensions.swift # Category DTO conformance │ └── Migrations/ # Database migrations -│ └── CreateUsersTableMigration.swift +│ ├── CreateUsersTableMigration.swift +│ └── CreateHabitsCategoryTableMigration.swift ├── Tests/ │ └── HabitTrackerAppServerTests/ │ ├── HabitTrackerAppServerTests.swift @@ -187,16 +193,20 @@ This project serves as a learning experience for backend development with Vapor. - ✅ User login endpoint with JWT token generation - ✅ Password hashing and verification - ✅ Database migration for users table +- ✅ Category model with database migration +- ✅ Category creation endpoint with JWT authentication - ✅ Swift 6.0 concurrency support (@Sendable) - ✅ Shared DTO package integration with @retroactive conformance - ✅ Test suite for authentication endpoints +- ✅ Test suite for category operations ### In Progress - 🔄 User logout endpoint - 🔄 Protected routes with JWT middleware ### Planned Features -- 📋 Categories and Habits CRUD operations +- 📋 Remaining Categories CRUD operations (GET, PUT, DELETE) +- 📋 Habits CRUD operations - 📋 Habit entries and calendar functionality - 📋 JWT token refresh endpoint From f15ffe5c61d8ecced04941b619a42c90e5cbcb10 Mon Sep 17 00:00:00 2001 From: dmakarau Date: Tue, 7 Oct 2025 22:33:24 +0200 Subject: [PATCH 7/8] Added checking for the uniqueness of the category --- .../Controllers/HabitsController.swift | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/Sources/HabitTrackerAppServer/Controllers/HabitsController.swift b/Sources/HabitTrackerAppServer/Controllers/HabitsController.swift index 0bfb9b8..1c256a0 100644 --- a/Sources/HabitTrackerAppServer/Controllers/HabitsController.swift +++ b/Sources/HabitTrackerAppServer/Controllers/HabitsController.swift @@ -24,25 +24,36 @@ struct HabitsController: RouteCollection { // get the user id guard let userId = req.parameters.get("userId", as: UUID.self) else { - throw Abort(.badRequest, reason: "Missing or invalid userId parameter") + throw Abort(.badRequest, reason: "Missing or invalid userId parameter") // HTTP 400 } // Decode request as a simple dictionary to get name and colorCode let requestData = try req.content.decode([String: String].self) guard let name = requestData["name"], let colorCode = requestData["colorCode"] else { - throw Abort(.badRequest, reason: "Missing required fields: name and colorCode") + throw Abort(.badRequest, reason: "Missing required fields: name and colorCode") // HTTP 400 } // Validate empty name guard !name.isEmpty else { - throw Abort(.badRequest, reason: "Category name cannot be empty") + throw Abort(.badRequest, reason: "Category name cannot be empty") // HTTP 400 } // Validate color code format (#RRGGBB) let colorCodePattern = #"^#([A-Fa-f0-9]{6})$"# guard colorCode.range(of: colorCodePattern, options: .regularExpression) != nil else { - throw Abort(.badRequest, reason: "Color code should be in format #RRGGBB") + throw Abort(.badRequest, reason: "Color code should be in format #RRGGBB") // HTTP 400 + } + + // Check for duplicate category name for this user (case-insensitive) + let existingCategories = try await Category.query(on: req.db) + .filter(\.$user.$id, .equal, userId) + .all() + + let existingCategory = existingCategories.first(where: { $0.name.lowercased() == name.lowercased() }) + + if existingCategory != nil { + throw Abort(.conflict, reason: "A category with this name already exists") // HTTP 409 } let habitCategory = Category( @@ -51,15 +62,15 @@ struct HabitsController: RouteCollection { userId: userId ) try await habitCategory.save(on: req.db) - + // After saving, ensure the ID is assigned guard habitCategory.id != nil else { - throw Abort(.internalServerError, reason: "Failed to get ID after saving category") + throw Abort(.internalServerError, reason: "Failed to get ID after saving category") // HTTP 500 } - + // DTO for the response guard let categoryResponseDTO = HabitsCategoryResponseDTO(habitCategory) else { - throw Abort(.internalServerError, reason: "Failed to create response DTO") + throw Abort(.internalServerError, reason: "Failed to create response DTO") // HTTP 500 } return categoryResponseDTO From 9e4e238e48354604fd79c96624134e8b44df0e11 Mon Sep 17 00:00:00 2001 From: dmakarau Date: Wed, 8 Oct 2025 12:17:46 +0200 Subject: [PATCH 8/8] Renamed the project --- .gitignore | 1 + Dockerfile | 8 +++---- Package.resolved | 8 +++---- Package.swift | 13 +++++----- README.md | 24 +++++++++---------- .../Controllers/.gitkeep | 0 .../Controllers/HabitsController.swift | 8 +++---- .../Controllers/UserController.swift | 4 ++-- ...HabitsCategoryResponseDTO+Extensions.swift | 22 +++++++++++++++++ .../LoginResponseDTO+Extensions.swift | 4 ++-- .../RegisterResponseDTO+Extensions.swift | 4 ++-- .../CreateHabitsCategoryTableMigration.swift | 2 +- .../CreateUsersTableMigration.swift | 2 +- .../Models/AuthPayload.swift | 2 +- .../Models/Category.swift | 2 +- .../Models/User.swift | 2 +- .../configure.swift | 0 .../entrypoint.swift | 0 .../routes.swift | 0 ...HabitsCategoryResponseDTO+Extensions.swift | 22 ----------------- .../GrowBitAppServerLoginTests.swift} | 10 ++++---- .../GrowBitAppServerRegistrationTests.swift} | 6 ++--- ...GrowBitAppServerSavingCategoryTests.swift} | 14 +++++------ docker-compose.yml | 2 +- 24 files changed, 81 insertions(+), 79 deletions(-) rename Sources/{HabitTrackerAppServer => GrowBitAppServer}/Controllers/.gitkeep (100%) rename Sources/{HabitTrackerAppServer => GrowBitAppServer}/Controllers/HabitsController.swift (93%) rename Sources/{HabitTrackerAppServer => GrowBitAppServer}/Controllers/UserController.swift (97%) create mode 100644 Sources/GrowBitAppServer/Extensions/HabitsCategoryResponseDTO+Extensions.swift rename Sources/{HabitTrackerAppServer => GrowBitAppServer}/Extensions/LoginResponseDTO+Extensions.swift (87%) rename Sources/{HabitTrackerAppServer => GrowBitAppServer}/Extensions/RegisterResponseDTO+Extensions.swift (88%) rename Sources/{HabitTrackerAppServer => GrowBitAppServer}/Migrations/CreateHabitsCategoryTableMigration.swift (96%) rename Sources/{HabitTrackerAppServer => GrowBitAppServer}/Migrations/CreateUsersTableMigration.swift (95%) rename Sources/{HabitTrackerAppServer => GrowBitAppServer}/Models/AuthPayload.swift (95%) rename Sources/{HabitTrackerAppServer => GrowBitAppServer}/Models/Category.swift (97%) rename Sources/{HabitTrackerAppServer => GrowBitAppServer}/Models/User.swift (97%) rename Sources/{HabitTrackerAppServer => GrowBitAppServer}/configure.swift (100%) rename Sources/{HabitTrackerAppServer => GrowBitAppServer}/entrypoint.swift (100%) rename Sources/{HabitTrackerAppServer => GrowBitAppServer}/routes.swift (100%) delete mode 100644 Sources/HabitTrackerAppServer/Extensions/HabitsCategoryResponseDTO+Extensions.swift rename Tests/{HabitTrackerAppServerTests/HabitTrackerAppServerLoginTests.swift => GrowBitAppServerTests/GrowBitAppServerLoginTests.swift} (95%) rename Tests/{HabitTrackerAppServerTests/HabitTrackerAppServerRegistrationTests.swift => GrowBitAppServerTests/GrowBitAppServerRegistrationTests.swift} (95%) rename Tests/{HabitTrackerAppServerTests/HabitTrackerAppServerSavingCategoryTests.swift => GrowBitAppServerTests/GrowBitAppServerSavingCategoryTests.swift} (95%) diff --git a/.gitignore b/.gitignore index 2fa01c4..e646ccc 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ db.sqlite .env.* !.env.example default.profraw +.vscode # Development guidance for Claude Code CLAUDE.md \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index ae47b74..f513929 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,11 +29,11 @@ RUN mkdir /staging # N.B.: The static version of jemalloc is incompatible with the static Swift runtime. RUN --mount=type=cache,target=/build/.build \ swift build -c release \ - --product HabitTrackerAppServer \ + --product GrowBitAppServer \ --static-swift-stdlib \ -Xlinker -ljemalloc && \ # Copy main executable to staging area - cp "$(swift build -c release --show-bin-path)/HabitTrackerAppServer" /staging && \ + cp "$(swift build -c release --show-bin-path)/GrowBitAppServer" /staging && \ # Copy resources bundled by SPM to staging area find -L "$(swift build -c release --show-bin-path)" -regex '.*\.resources$' -exec cp -Ra {} /staging \; @@ -48,7 +48,7 @@ RUN cp "/usr/libexec/swift/linux/swift-backtrace-static" ./ RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; }; \ [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w ./Resources; }; \ # Make it executable - chmod +x HabitTrackerAppServer; \ + chmod +x GrowBitAppServer; \ # Create the vapor user and group with /vapor home directory useradd --user-group --create-home --system --skel /dev/null --home-dir /vapor vapor @@ -64,5 +64,5 @@ RUN chown -R vapor:vapor /vapor USER vapor:vapor # Start the Vapor service when the image is run, default to listening on 8080 in production environment -ENTRYPOINT ["./HabitTrackerAppServer"] +ENTRYPOINT ["./GrowBitAppServer"] CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] \ No newline at end of file diff --git a/Package.resolved b/Package.resolved index 4a2e0bc..b2af171 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "b3513f2753a0dd43441713a534385b8dee7bdf3f4d6bd2c6b9d224ad321195f6", + "originHash" : "790b17c32995ac1583b6f3ad5a89c48f9af9e412b7c71c692b3de104b7435fd5", "pins" : [ { "identity" : "async-http-client", @@ -65,12 +65,12 @@ } }, { - "identity" : "habittrackerappshareddto", + "identity" : "growbitshareddto", "kind" : "remoteSourceControl", - "location" : "https://github.com/dmakarau/HabitTrackerAppSharedDTO.git", + "location" : "https://github.com/dmakarau/GrowBitSharedDTO", "state" : { "branch" : "main", - "revision" : "f93b5fbb5d9435537ca68d80040a78478634bf0a" + "revision" : "8d00b4d406269648f863d4e6cf02e33073445eb7" } }, { diff --git a/Package.swift b/Package.swift index 55deb44..090c031 100644 --- a/Package.swift +++ b/Package.swift @@ -2,7 +2,7 @@ import PackageDescription let package = Package( - name: "HabitTrackerAppServer", + name: "GrowBitAppServer", platforms: [ .macOS(.v13) ], @@ -13,18 +13,18 @@ let package = Package( .package(url: "https://github.com/vapor/fluent.git", from: "4.12.0"), .package(url: "https://github.com/vapor/fluent-postgres-driver", from: "2.11.0"), .package(url: "https://github.com/vapor/fluent-sqlite-driver", from: "4.8.0"), - .package(url: "https://github.com/dmakarau/HabitTrackerAppSharedDTO.git", branch: "main"), + .package(url: "https://github.com/dmakarau/GrowBitSharedDTO", branch: "main"), // 🔵 Non-blocking, event-driven networking for Swift. Used for custom executors .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), ], targets: [ .executableTarget( - name: "HabitTrackerAppServer", + name: "GrowBitAppServer", dependencies: [ .product(name: "Vapor", package: "vapor"), .product(name: "JWT", package: "jwt"), - .product(name: "HabitTrackerAppSharedDTO", package: "HabitTrackerAppSharedDTO"), + .product(name: "GrowBitSharedDTO", package: "GrowBitSharedDTO"), .product(name: "Fluent", package: "fluent"), .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"), .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), @@ -34,10 +34,11 @@ let package = Package( swiftSettings: swiftSettings ), .testTarget( - name: "HabitTrackerAppServerTests", + name: "GrowBitAppServerTests", dependencies: [ - .target(name: "HabitTrackerAppServer"), + .target(name: "GrowBitAppServer"), .product(name: "VaporTesting", package: "vapor"), + .product(name: "GrowBitSharedDTO", package: "GrowBitSharedDTO"), ], swiftSettings: swiftSettings ) diff --git a/README.md b/README.md index 0efa3c3..87c565e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# HabitTracker API Server +# GrowBit API Server A Swift-based REST API server for habit tracking, built with the Vapor web framework. @@ -12,7 +12,7 @@ A Swift-based REST API server for habit tracking, built with the Vapor web frame - SQLite (development) - **Swift Version**: 6.0+ - **Platform**: macOS 13+ -- **Shared DTOs**: HabitTrackerAppSharedDTO (external package) +- **Shared DTOs**: GrowBitSharedDTO (external package) ## Features @@ -67,8 +67,8 @@ A Swift-based REST API server for habit tracking, built with the Vapor web frame 1. Clone the repository: ```bash -git clone -cd HabitTracker-API-Server +git clone https://github.com/dmakarau/GrowBit-API-Server.git +cd GrowBit-API-Server ``` 2. Resolve dependencies: @@ -91,7 +91,7 @@ openssl rand -base64 32 #### Development Mode ```bash -swift run HabitTrackerAppServer serve --hostname 0.0.0.0 --port 8080 +swift run GrowBitAppServer serve --hostname 0.0.0.0 --port 8080 ``` #### Using Docker @@ -116,10 +116,10 @@ swift test ## Project Structure ``` -HabitTracker-API-Server/ +GrowBit-API-Server/ ├── Package.swift # Swift Package Manager configuration ├── Sources/ -│ └── HabitTrackerAppServer/ +│ └── GrowBitAppServer/ │ ├── entrypoint.swift # Application entry point │ ├── configure.swift # Application configuration │ ├── routes.swift # Route definitions @@ -133,14 +133,14 @@ HabitTracker-API-Server/ │ ├── Extensions/ # Protocol conformances for shared types │ │ ├── RegisterResponseDTO+Extensions.swift # Vapor Content conformance │ │ ├── LoginResponseDTO+Extensions.swift # Vapor Content conformance -│ │ └── HabitsCategoryResponseDTO+Extensions.swift # Category DTO conformance +│ │ └── CategoryResponseDTO+Extensions.swift # Category DTO conformance │ └── Migrations/ # Database migrations │ ├── CreateUsersTableMigration.swift │ └── CreateHabitsCategoryTableMigration.swift ├── Tests/ -│ └── HabitTrackerAppServerTests/ -│ ├── HabitTrackerAppServerTests.swift -│ └── HabitTrackerAppServerLoginTests.swift +│ └── GrowBitAppServerTests/ +│ ├── GrowBitAppServerTests.swift +│ └── GrowBitAppServerLoginTests.swift ├── Public/ # Static files directory ├── Dockerfile # Docker configuration ├── docker-compose.yml # Docker Compose configuration @@ -153,7 +153,7 @@ Create a `.env` file in the root directory with the following variables: ```bash # Database Configuration -DATABASE_URL=postgresql://username:password@localhost:5432/habittracker_db +DATABASE_URL=postgresql://username:password@localhost:5432/growbit_db # JWT Configuration JWT_SECRET=your-super-secret-jwt-key-here diff --git a/Sources/HabitTrackerAppServer/Controllers/.gitkeep b/Sources/GrowBitAppServer/Controllers/.gitkeep similarity index 100% rename from Sources/HabitTrackerAppServer/Controllers/.gitkeep rename to Sources/GrowBitAppServer/Controllers/.gitkeep diff --git a/Sources/HabitTrackerAppServer/Controllers/HabitsController.swift b/Sources/GrowBitAppServer/Controllers/HabitsController.swift similarity index 93% rename from Sources/HabitTrackerAppServer/Controllers/HabitsController.swift rename to Sources/GrowBitAppServer/Controllers/HabitsController.swift index 1c256a0..1c5ece4 100644 --- a/Sources/HabitTrackerAppServer/Controllers/HabitsController.swift +++ b/Sources/GrowBitAppServer/Controllers/HabitsController.swift @@ -1,13 +1,13 @@ // // HabitsController.swift -// HabitTrackerAppServer +// GrowBitAppServer // // Created by Denis Makarau on 03.10.25. // import Foundation import Vapor -import HabitTrackerAppSharedDTO +import GrowBitSharedDTO struct HabitsController: RouteCollection { func boot(routes: any RoutesBuilder) throws { @@ -20,7 +20,7 @@ struct HabitsController: RouteCollection { api.post("categories", use: saveHabitCategory) } - @Sendable func saveHabitCategory(req: Request) async throws -> HabitsCategoryResponseDTO { + @Sendable func saveHabitCategory(req: Request) async throws -> CategoryResponseDTO { // get the user id guard let userId = req.parameters.get("userId", as: UUID.self) else { @@ -69,7 +69,7 @@ struct HabitsController: RouteCollection { } // DTO for the response - guard let categoryResponseDTO = HabitsCategoryResponseDTO(habitCategory) else { + guard let categoryResponseDTO = CategoryResponseDTO(habitCategory) else { throw Abort(.internalServerError, reason: "Failed to create response DTO") // HTTP 500 } diff --git a/Sources/HabitTrackerAppServer/Controllers/UserController.swift b/Sources/GrowBitAppServer/Controllers/UserController.swift similarity index 97% rename from Sources/HabitTrackerAppServer/Controllers/UserController.swift rename to Sources/GrowBitAppServer/Controllers/UserController.swift index 51bb1ca..0a88040 100644 --- a/Sources/HabitTrackerAppServer/Controllers/UserController.swift +++ b/Sources/GrowBitAppServer/Controllers/UserController.swift @@ -1,6 +1,6 @@ // // UserController.swift -// HabitTrackerAppServer +// GrowBitAppServer // // Created by Denis Makarau on 24.09.25. // @@ -8,7 +8,7 @@ import Foundation import Vapor import Fluent -import HabitTrackerAppSharedDTO +import GrowBitSharedDTO struct UserController: RouteCollection { func boot(routes: any Vapor.RoutesBuilder) throws { diff --git a/Sources/GrowBitAppServer/Extensions/HabitsCategoryResponseDTO+Extensions.swift b/Sources/GrowBitAppServer/Extensions/HabitsCategoryResponseDTO+Extensions.swift new file mode 100644 index 0000000..117d5dc --- /dev/null +++ b/Sources/GrowBitAppServer/Extensions/HabitsCategoryResponseDTO+Extensions.swift @@ -0,0 +1,22 @@ +// +// HabitsCategoryResponseDTO+Extensions.swift +// GrowBitAppServer +// +// Created by Denis Makarau on 06.10.25. +// + +import Foundation +import GrowBitSharedDTO +import Vapor + +extension CategoryResponseDTO: @retroactive RequestDecodable {} +extension CategoryResponseDTO: @retroactive ResponseEncodable {} +extension CategoryResponseDTO: @retroactive AsyncRequestDecodable {} +extension CategoryResponseDTO: @retroactive AsyncResponseEncodable {} +extension CategoryResponseDTO: @retroactive Content { + init?(_ category: Category) { + guard let id = category.id else { return nil } + + self.init(id: id, name: category.name, colorCode: category.colorCode) + } +} diff --git a/Sources/HabitTrackerAppServer/Extensions/LoginResponseDTO+Extensions.swift b/Sources/GrowBitAppServer/Extensions/LoginResponseDTO+Extensions.swift similarity index 87% rename from Sources/HabitTrackerAppServer/Extensions/LoginResponseDTO+Extensions.swift rename to Sources/GrowBitAppServer/Extensions/LoginResponseDTO+Extensions.swift index b063610..6431ea6 100644 --- a/Sources/HabitTrackerAppServer/Extensions/LoginResponseDTO+Extensions.swift +++ b/Sources/GrowBitAppServer/Extensions/LoginResponseDTO+Extensions.swift @@ -1,12 +1,12 @@ // // RegisterResponseDTO.swift -// HabitTrackerAppServer +// GrowBitAppServer // // Created by Denis Makarau on 03.10.25. // import Foundation -import HabitTrackerAppSharedDTO +import GrowBitSharedDTO import Vapor extension LoginResponseDTO: @retroactive RequestDecodable {} diff --git a/Sources/HabitTrackerAppServer/Extensions/RegisterResponseDTO+Extensions.swift b/Sources/GrowBitAppServer/Extensions/RegisterResponseDTO+Extensions.swift similarity index 88% rename from Sources/HabitTrackerAppServer/Extensions/RegisterResponseDTO+Extensions.swift rename to Sources/GrowBitAppServer/Extensions/RegisterResponseDTO+Extensions.swift index b8dd224..fb03cf5 100644 --- a/Sources/HabitTrackerAppServer/Extensions/RegisterResponseDTO+Extensions.swift +++ b/Sources/GrowBitAppServer/Extensions/RegisterResponseDTO+Extensions.swift @@ -1,13 +1,13 @@ // // RegisterResponseDTO.swift -// HabitTrackerAppServer +// GrowBitAppServer // // Created by Denis Makarau on 03.10.25. // import Foundation import Vapor -import HabitTrackerAppSharedDTO +import GrowBitSharedDTO extension RegisterResponseDTO: @retroactive RequestDecodable {} extension RegisterResponseDTO: @retroactive ResponseEncodable {} diff --git a/Sources/HabitTrackerAppServer/Migrations/CreateHabitsCategoryTableMigration.swift b/Sources/GrowBitAppServer/Migrations/CreateHabitsCategoryTableMigration.swift similarity index 96% rename from Sources/HabitTrackerAppServer/Migrations/CreateHabitsCategoryTableMigration.swift rename to Sources/GrowBitAppServer/Migrations/CreateHabitsCategoryTableMigration.swift index d624284..8483321 100644 --- a/Sources/HabitTrackerAppServer/Migrations/CreateHabitsCategoryTableMigration.swift +++ b/Sources/GrowBitAppServer/Migrations/CreateHabitsCategoryTableMigration.swift @@ -1,6 +1,6 @@ // // CreateHabitsCategoryTableMigration.swift -// HabitTrackerAppServer +// GrowBitAppServer // // Created by Denis Makarau on 03.10.25. // diff --git a/Sources/HabitTrackerAppServer/Migrations/CreateUsersTableMigration.swift b/Sources/GrowBitAppServer/Migrations/CreateUsersTableMigration.swift similarity index 95% rename from Sources/HabitTrackerAppServer/Migrations/CreateUsersTableMigration.swift rename to Sources/GrowBitAppServer/Migrations/CreateUsersTableMigration.swift index eeecc94..7527ff8 100644 --- a/Sources/HabitTrackerAppServer/Migrations/CreateUsersTableMigration.swift +++ b/Sources/GrowBitAppServer/Migrations/CreateUsersTableMigration.swift @@ -1,6 +1,6 @@ // // CreateUsersTableMigration.swift.swift -// HabitTrackerAppServer +// GrowBitAppServer // // Created by Denis Makarau on 24.09.25. // diff --git a/Sources/HabitTrackerAppServer/Models/AuthPayload.swift b/Sources/GrowBitAppServer/Models/AuthPayload.swift similarity index 95% rename from Sources/HabitTrackerAppServer/Models/AuthPayload.swift rename to Sources/GrowBitAppServer/Models/AuthPayload.swift index cf78c91..02761e6 100644 --- a/Sources/HabitTrackerAppServer/Models/AuthPayload.swift +++ b/Sources/GrowBitAppServer/Models/AuthPayload.swift @@ -1,6 +1,6 @@ // // AuthPayload.swift -// HabitTrackerAppServer +// GrowBitAppServer // // Created by Denis Makarau on 25.09.25. // diff --git a/Sources/HabitTrackerAppServer/Models/Category.swift b/Sources/GrowBitAppServer/Models/Category.swift similarity index 97% rename from Sources/HabitTrackerAppServer/Models/Category.swift rename to Sources/GrowBitAppServer/Models/Category.swift index bc804e0..a4a7696 100644 --- a/Sources/HabitTrackerAppServer/Models/Category.swift +++ b/Sources/GrowBitAppServer/Models/Category.swift @@ -1,5 +1,5 @@ // Category.swift -// HabitTrackerAppServer +// GrowBitAppServer // // Created by Denis Makarau on 03.10.25. // diff --git a/Sources/HabitTrackerAppServer/Models/User.swift b/Sources/GrowBitAppServer/Models/User.swift similarity index 97% rename from Sources/HabitTrackerAppServer/Models/User.swift rename to Sources/GrowBitAppServer/Models/User.swift index 57d4e8a..af45be8 100644 --- a/Sources/HabitTrackerAppServer/Models/User.swift +++ b/Sources/GrowBitAppServer/Models/User.swift @@ -1,6 +1,6 @@ // // User.swift -// HabitTrackerAppServer +// GrowBitAppServer // // Created by Denis Makarau on 24.09.25. // diff --git a/Sources/HabitTrackerAppServer/configure.swift b/Sources/GrowBitAppServer/configure.swift similarity index 100% rename from Sources/HabitTrackerAppServer/configure.swift rename to Sources/GrowBitAppServer/configure.swift diff --git a/Sources/HabitTrackerAppServer/entrypoint.swift b/Sources/GrowBitAppServer/entrypoint.swift similarity index 100% rename from Sources/HabitTrackerAppServer/entrypoint.swift rename to Sources/GrowBitAppServer/entrypoint.swift diff --git a/Sources/HabitTrackerAppServer/routes.swift b/Sources/GrowBitAppServer/routes.swift similarity index 100% rename from Sources/HabitTrackerAppServer/routes.swift rename to Sources/GrowBitAppServer/routes.swift diff --git a/Sources/HabitTrackerAppServer/Extensions/HabitsCategoryResponseDTO+Extensions.swift b/Sources/HabitTrackerAppServer/Extensions/HabitsCategoryResponseDTO+Extensions.swift deleted file mode 100644 index 87e194b..0000000 --- a/Sources/HabitTrackerAppServer/Extensions/HabitsCategoryResponseDTO+Extensions.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// HabitsCategoryResponseDTO+Extensions.swift -// HabitTrackerAppServer -// -// Created by Denis Makarau on 06.10.25. -// - -import Foundation -import HabitTrackerAppSharedDTO -import Vapor - -extension HabitsCategoryResponseDTO: @retroactive RequestDecodable {} -extension HabitsCategoryResponseDTO: @retroactive ResponseEncodable {} -extension HabitsCategoryResponseDTO: @retroactive AsyncRequestDecodable {} -extension HabitsCategoryResponseDTO: @retroactive AsyncResponseEncodable {} -extension HabitsCategoryResponseDTO: @retroactive Content { - init?(_ category: Category) { - guard let id = category.id else { return nil } - - self.init(id: id, name: category.name, colorCode: category.colorCode) - } -} diff --git a/Tests/HabitTrackerAppServerTests/HabitTrackerAppServerLoginTests.swift b/Tests/GrowBitAppServerTests/GrowBitAppServerLoginTests.swift similarity index 95% rename from Tests/HabitTrackerAppServerTests/HabitTrackerAppServerLoginTests.swift rename to Tests/GrowBitAppServerTests/GrowBitAppServerLoginTests.swift index bbd0dc5..d66fdce 100644 --- a/Tests/HabitTrackerAppServerTests/HabitTrackerAppServerLoginTests.swift +++ b/Tests/GrowBitAppServerTests/GrowBitAppServerLoginTests.swift @@ -1,18 +1,18 @@ // -// HabitTrackerAppServerLoginTests.swift -// HabitTrackerAppServer +// GrowBitAppServerLoginTests.swift +// GrowBitAppServer // // Created by Denis Makarau on 26.09.25. // import Foundation -@testable import HabitTrackerAppServer -import HabitTrackerAppSharedDTO +@testable import GrowBitAppServer +import GrowBitSharedDTO import VaporTesting import Testing @Suite("App Login Tests") -struct HabitTrackerAppServerLoginTests { +struct GrowBitAppServerLoginTests { private func createUser(in app: Application, username: String = "testuser") async throws { let requestBody = User(username: username, password: "password") diff --git a/Tests/HabitTrackerAppServerTests/HabitTrackerAppServerRegistrationTests.swift b/Tests/GrowBitAppServerTests/GrowBitAppServerRegistrationTests.swift similarity index 95% rename from Tests/HabitTrackerAppServerTests/HabitTrackerAppServerRegistrationTests.swift rename to Tests/GrowBitAppServerTests/GrowBitAppServerRegistrationTests.swift index 1e1f1c6..2607cdb 100644 --- a/Tests/HabitTrackerAppServerTests/HabitTrackerAppServerRegistrationTests.swift +++ b/Tests/GrowBitAppServerTests/GrowBitAppServerRegistrationTests.swift @@ -1,10 +1,10 @@ -@testable import HabitTrackerAppServer +@testable import GrowBitAppServer import VaporTesting -import HabitTrackerAppSharedDTO +import GrowBitSharedDTO import Testing @Suite("App Registration Tests") -struct HabitTrackerAppServerRegistrationTests { +struct GrowBitAppServerRegistrationTests { @Test("Test Hello World Route") func helloWorld() async throws { try await withApp(configure: configure) { app in diff --git a/Tests/HabitTrackerAppServerTests/HabitTrackerAppServerSavingCategoryTests.swift b/Tests/GrowBitAppServerTests/GrowBitAppServerSavingCategoryTests.swift similarity index 95% rename from Tests/HabitTrackerAppServerTests/HabitTrackerAppServerSavingCategoryTests.swift rename to Tests/GrowBitAppServerTests/GrowBitAppServerSavingCategoryTests.swift index 5c3eb9a..894389b 100644 --- a/Tests/HabitTrackerAppServerTests/HabitTrackerAppServerSavingCategoryTests.swift +++ b/Tests/GrowBitAppServerTests/GrowBitAppServerSavingCategoryTests.swift @@ -1,18 +1,18 @@ // -// HabitTrackerAppServerSavingCategoryTests.swift -// HabitTrackerAppServer +// GrowBitAppServerSavingCategoryTests.swift +// GrowBitAppServer // // Created by Denis Makarau on 06.10.25. // -@testable import HabitTrackerAppServer +@testable import GrowBitAppServer import VaporTesting -import HabitTrackerAppSharedDTO +import GrowBitSharedDTO import Testing import Fluent @Suite("Category Creation Tests") -struct HabitTrackerAppServerSavingCategoryTests { +struct GrowBitAppServerSavingCategoryTests { @Test("Category creation - Success") func categoryCreationSuccess() async throws { @@ -45,7 +45,7 @@ struct HabitTrackerAppServerSavingCategoryTests { try req.content.encode(requestBody) } afterResponse: { res in #expect(res.status == .ok) - let response = try res.content.decode(HabitsCategoryResponseDTO.self) + let response = try res.content.decode(CategoryResponseDTO.self) #expect(response.name == "test category") #expect(response.colorCode == "#FFFFFF") } @@ -227,7 +227,7 @@ struct HabitTrackerAppServerSavingCategoryTests { try req.content.encode(requestBody) } afterResponse: { res in #expect(res.status == .ok) - let response = try res.content.decode(HabitsCategoryResponseDTO.self) + let response = try res.content.decode(CategoryResponseDTO.self) #expect(response.colorCode.uppercased() == color.uppercased()) } } diff --git a/docker-compose.yml b/docker-compose.yml index 67229a6..8e64dd9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,7 +18,7 @@ x-shared_environment: &shared_environment services: app: - image: habit-tracker-app-server:latest + image: growbit-app-server:latest build: context: . environment: