diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index d72d2f2..35a9d31 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -47,6 +47,7 @@ jobs: type=ref,event=branch type=semver,pattern={{version}} type=sha + type=raw,value=prod # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If # the build succeeds, it pushes the image to GitHub Packages. It uses the `context` parameter to define the build's context # as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" diff --git a/README.md b/README.md new file mode 100644 index 0000000..18f6b29 --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# vapor-po + +The website for generating purchase orders. + +## Usage + +Generally the app should be ran through a docker container or docker-compose file. Examples are in +the `./docker` folder. + +Images get built in the `CI` environment when a tag is pushed to the repository. + +### Getting Started + +When the application is first launched an admin user should be created in the running container. +Attach to the container using `docker exec` or `docker compose exec`, then run: + +``` +./App generate-admin --username "admin" --password "super-secret --confirmPassword "super-secret" +``` + +You can then login and generate user, employees, vendors, etc. + +After the setup has been completed, then you should generate a mock purchase order and set the `id` +to the value you would like new purchase orders to start from. This should be done through calling +the api, as the web interface does not allow users to enter an id value. + +#### Example + +These examples use `httpie`, note the port is used for local development, in a production +environment you would just use the FQDN of where the application is running. + +**Login** + +``` +http :8080/api/v1/login username="admin" password="super-secret" \ + | jq '.["token"]' \ + | pbcopy +``` + +**Set the token as environment variable** + +``` +export API_TOKEN= +``` + +**Get the employees to copy an id to use for the purchase order** + +``` +http -A bearer -a "$API_TOKEN" :8080/api/v1/employees +``` + +**Get the vendor branches to copy an id to use for the purchase order** + +``` +http -A bearer -a "$API_TOKEN" :8080/api/v1/vendors/branches +``` + +**Generate first po** + +``` +http -A bearer -a "$API_TOKEN" :8080/api/v1/purchase-orders \ + id:="60000" \ + materials="Test" \ + customer="Testy McTestface" \ + createdForID="" + vendorBranchID="" +``` diff --git a/Sources/SharedModels/Employee.swift b/Sources/SharedModels/Employee.swift index b0e9159..036de88 100644 --- a/Sources/SharedModels/Employee.swift +++ b/Sources/SharedModels/Employee.swift @@ -1,6 +1,9 @@ import Dependencies import Foundation +/// Represents an employee database model. +/// +/// Employee's are who purchase orders can be generated for. public struct Employee: Codable, Equatable, Identifiable, Sendable { public var id: UUID public var active: Bool @@ -31,6 +34,8 @@ public struct Employee: Codable, Equatable, Identifiable, Sendable { } public extension Employee { + /// Represents the required fields for generating a new employee in the + /// database. struct Create: Codable, Sendable, Equatable { public let firstName: String public let lastName: String @@ -47,6 +52,8 @@ public extension Employee { } } + /// Represents the required fields for updating an existing employee in the + /// database. struct Update: Codable, Sendable, Equatable { public let firstName: String? public let lastName: String? diff --git a/Sources/SharedModels/PurchaseOrder.swift b/Sources/SharedModels/PurchaseOrder.swift index 4fe5e1d..7adcea7 100644 --- a/Sources/SharedModels/PurchaseOrder.swift +++ b/Sources/SharedModels/PurchaseOrder.swift @@ -1,6 +1,11 @@ import Dependencies import Foundation +/// Represents a purchase order database model. +/// +/// A purchase order is generated on behalf of an `Employee` and issued to +/// a `VendorBranch`. It includes information about the customer / job it was created +/// for, the materials that were purchased, etc. public struct PurchaseOrder: Codable, Equatable, Identifiable, Sendable { public let id: Int @@ -41,6 +46,7 @@ public struct PurchaseOrder: Codable, Equatable, Identifiable, Sendable { public extension PurchaseOrder { + /// Represents the required fields for generating a new purchase order in the database. struct Create: Codable, Sendable, Equatable { public let id: Int? @@ -73,6 +79,9 @@ public extension PurchaseOrder { } } + /// Represents the required fields for generating a new purchase order in the database, + /// without the user information who is issuing the request, which get's parsed from the + /// currently authenticated user's session and is used to generate the full `Create` request. struct CreateIntermediate: Codable, Sendable, Equatable { public let id: Int? @@ -115,6 +124,8 @@ public extension PurchaseOrder { } } + /// Represents the context to search or filter purchase orders based on the + /// given parameters. enum SearchContext: Sendable, Equatable { case customer(String) case vendor(VendorBranch.ID) diff --git a/Sources/SharedModels/Routes/ApiRoute.swift b/Sources/SharedModels/Routes/ApiRoute.swift index 998936d..0c0e6b5 100644 --- a/Sources/SharedModels/Routes/ApiRoute.swift +++ b/Sources/SharedModels/Routes/ApiRoute.swift @@ -4,6 +4,9 @@ import Foundation public extension SiteRoute { + /// Represents api routes that can be interacted with. + /// + /// These routes return json information, as opposed to html like the view routes. enum Api: Sendable, Equatable { case employee(EmployeeRoute) diff --git a/Sources/SharedModels/Routes/SiteRoute.swift b/Sources/SharedModels/Routes/SiteRoute.swift index 9be8955..95a7d22 100644 --- a/Sources/SharedModels/Routes/SiteRoute.swift +++ b/Sources/SharedModels/Routes/SiteRoute.swift @@ -2,6 +2,7 @@ import CasePathsCore import Foundation @preconcurrency import URLRouting +/// Represents all the routes that our server can handle. public enum SiteRoute: Sendable { case api(SiteRoute.Api) case health diff --git a/Sources/SharedModels/Routes/ViewRoute.swift b/Sources/SharedModels/Routes/ViewRoute.swift index 7cd2c1a..a85c77c 100644 --- a/Sources/SharedModels/Routes/ViewRoute.swift +++ b/Sources/SharedModels/Routes/ViewRoute.swift @@ -4,6 +4,10 @@ import Foundation public extension SiteRoute { // swiftlint:disable type_body_length + + /// Represents view routes that can be interacted with. + /// + /// These routes return html and are used to generate the web interface. enum View: Sendable, Equatable { case employee(SiteRoute.View.EmployeeRoute) diff --git a/Sources/SharedModels/User.swift b/Sources/SharedModels/User.swift index bb418ad..5cb6aa4 100644 --- a/Sources/SharedModels/User.swift +++ b/Sources/SharedModels/User.swift @@ -1,6 +1,11 @@ import Dependencies import Foundation +/// Represents a user database model. +/// +/// User's are who can login to the system and generate purchase orders, manage +/// employees, vendors, etc. +/// public struct User: Codable, Equatable, Identifiable, Sendable { public var id: UUID @@ -26,6 +31,7 @@ public struct User: Codable, Equatable, Identifiable, Sendable { public extension User { + /// Represents the fields needed to generate a new user in the database. struct Create: Codable, Sendable, Equatable { public let username: String public let email: String @@ -45,6 +51,7 @@ public extension User { } } + /// Represents the fields needed for new user to login. struct Login: Codable, Sendable, Equatable { public let username: String? public let email: String? @@ -61,6 +68,7 @@ public extension User { } } + /// Represents the fields needed to reset the password of a user. struct ResetPassword: Codable, Equatable, Sendable { public let password: String public let confirmPassword: String @@ -74,6 +82,8 @@ public extension User { } } + /// Represents a user token that can be used to authenticate a user, typically + /// used for interacting with api routes remotely. struct Token: Codable, Equatable, Identifiable, Sendable { public let id: UUID public let userID: User.ID @@ -90,6 +100,7 @@ public extension User { } } + /// Represents the fields needed to update a user's attributes in the database. struct Update: Codable, Equatable, Sendable { public let username: String? public let email: String? diff --git a/Sources/SharedModels/Vendor.swift b/Sources/SharedModels/Vendor.swift index dce4341..cdbf9fe 100644 --- a/Sources/SharedModels/Vendor.swift +++ b/Sources/SharedModels/Vendor.swift @@ -1,6 +1,10 @@ import Dependencies import Foundation +/// Represents a vendor item in the database. +/// +/// A vendor is parent item that contains one or more branches where purchase orders +/// can be issued to. It is primarily a name space to group related branches together. public struct Vendor: Codable, Equatable, Identifiable, Sendable { public var id: UUID public var name: String @@ -25,6 +29,7 @@ public struct Vendor: Codable, Equatable, Identifiable, Sendable { public extension Vendor { + /// Represents the fields required to generate a new vendor in the database. struct Create: Codable, Sendable, Equatable { public let name: String @@ -33,6 +38,7 @@ public extension Vendor { } } + /// Represents the fields required to update a vendor in the database. struct Update: Codable, Sendable, Equatable { public let name: String diff --git a/Sources/SharedModels/VendorBranch.swift b/Sources/SharedModels/VendorBranch.swift index a82b775..b666ade 100644 --- a/Sources/SharedModels/VendorBranch.swift +++ b/Sources/SharedModels/VendorBranch.swift @@ -1,6 +1,10 @@ import Dependencies import Foundation +/// Represents a vendor branch database model. +/// +/// A vendor branch is who purchase orders can be issued to on behalf an `Employee`. +/// They are associated with a particular `Vendor`. public struct VendorBranch: Codable, Equatable, Identifiable, Sendable { public var id: UUID public var name: String @@ -24,6 +28,8 @@ public struct VendorBranch: Codable, Equatable, Identifiable, Sendable { } public extension VendorBranch { + + /// Represents the fields required to generate a new vendor branch in the database. struct Create: Codable, Sendable, Equatable { public let name: String public let vendorID: Vendor.ID @@ -34,6 +40,10 @@ public extension VendorBranch { } } + /// Represents the details of a vendor branch, which includes the parent vendor item. + /// + /// This is used in several of the views / api routes that require information about both the + /// vendor branch and it's associated parent vendor item. struct Detail: Codable, Equatable, Identifiable, Sendable { public var id: UUID public var name: String @@ -56,6 +66,7 @@ public extension VendorBranch { } } + /// Represents the fields that are used to update attributes of a vendor branch in the database. struct Update: Codable, Sendable, Equatable { public let name: String? diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 4abb55a..3a6de32 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -21,7 +21,8 @@ services: app: image: hhe-po:latest build: - context: . + context: .. + dockerfile: ./docker/Dockerfile environment: <<: *shared_environment volumes: @@ -30,12 +31,20 @@ services: - '8080:8080' labels: - dev.orbstack.domains=po.local + healthcheck: + test: curl --fail -s http://0.0.0.0:8080/health || exit 1 + interval: 1m30s + timeout: 10s + retries: 3 + deploy: + replicas: 3 # user: '0' # uncomment to run as root for testing purposes even though Dockerfile defines 'vapor' user. command: ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] migrate: image: hhe-po:latest build: - context: . + context: .. + dockerfile: ./docker/Dockerfile environment: <<: *shared_environment command: ["migrate", "--yes"] @@ -46,7 +55,8 @@ services: revert: image: hhe-po:latest build: - context: . + context: .. + dockerfile: ./docker/Dockerfile environment: <<: *shared_environment command: ["migrate", "--revert", "--yes"]