diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..4f7c6a2 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,20 @@ +name: CI + +on: + push: + workflow_dispatch: + +jobs: + ubuntu: + name: Ubuntu + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Setup just. + uses: https://git.housh.dev/actions/setup-just.git@v1 + - name: Setup QEMU + uses: docker/setup-qemu-action@v3 + - name: Setup buildx + uses: docker/setup-buildx-action@v3 + - name: Run tests. + run: just test-docker diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..72e8c6e --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,63 @@ +name: Create and publish a Docker image + +# Configures this workflow to run every time a change is pushed to the branch called `release`. +on: + push: + branches: ['main'] + tags: + - '*.*.*' + workflow_dispatch: + +# Defines two custom environment variables for the workflow. These are used for the Container registry domain, +# and a name for the Docker image that this workflow builds. +env: + REGISTRY: git.housh.dev + IMAGE_NAME: ${{ gitea.repository }} + +# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. +jobs: + build-and-push-image: + runs-on: ubuntu-latest + # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. + permissions: + contents: read + packages: write + attestations: write + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + # Uses the `docker/login-action` action to log in to the Container registry registry using the account + # and password that will publish the packages. Once published, the packages are scoped to the account defined here. + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ gitea.actor }} + password: ${{ secrets.CONTAINER_TOKEN }} + # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels + # that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a + # subsequent step. The `images` value provides the base name for the tags and labels. + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=sha + # 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)" + # in the README of the `docker/build-push-action` repository. + # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. + - name: Build and push Docker image + id: push + uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/Sources/App/Dependencies/DateFormatter.swift b/Sources/App/Dependencies/DateFormatter.swift deleted file mode 100644 index dc7848a..0000000 --- a/Sources/App/Dependencies/DateFormatter.swift +++ /dev/null @@ -1,28 +0,0 @@ -// import Dependencies -// import Foundation -// -// public extension DependencyValues { -// var dateFormatter: DateFormatter { -// get { self[DateFormatter.self] } -// set { self[DateFormatter.self] = newValue } -// } -// } -// -// #if hasFeature(RetroactiveAttribute) -// extension DateFormatter: @retroactive DependencyKey { -// -// public static var liveValue: DateFormatter { -// let formatter = DateFormatter() -// formatter.dateStyle = .short -// return formatter -// } -// } -// #else -// extension DateFormatter: DependencyKey { -// public static var liveValue: DateFormatter { -// let formatter = DateFormatter() -// formatter.dateStyle = .short -// return formatter -// } -// } -// #endif diff --git a/Sources/DatabaseClientLive/Employees+live.swift b/Sources/DatabaseClientLive/Employees+live.swift index 7420ea8..a3c6d46 100644 --- a/Sources/DatabaseClientLive/Employees+live.swift +++ b/Sources/DatabaseClientLive/Employees+live.swift @@ -127,10 +127,10 @@ final class EmployeeModel: Model, @unchecked Sendable { @Field(key: "is_active") var active: Bool - @Timestamp(key: "created_at", on: .create) + @Timestamp(key: "created_at", on: .create, format: .iso8601) var createdAt: Date? - @Timestamp(key: "updated_at", on: .update) + @Timestamp(key: "updated_at", on: .update, format: .iso8601) var updatedAt: Date? init() {} diff --git a/Sources/DatabaseClientLive/PurchaseOrders+live.swift b/Sources/DatabaseClientLive/PurchaseOrders+live.swift index 506b19a..9f5aaab 100644 --- a/Sources/DatabaseClientLive/PurchaseOrders+live.swift +++ b/Sources/DatabaseClientLive/PurchaseOrders+live.swift @@ -142,10 +142,10 @@ final class PurchaseOrderModel: Model, Codable, @unchecked Sendable { @Parent(key: "vendor_branch_id") var vendorBranch: VendorBranchModel - @Timestamp(key: "created_at", on: .create) + @Timestamp(key: "created_at", on: .create, format: .iso8601) var createdAt: Date? - @Timestamp(key: "updated_at", on: .update) + @Timestamp(key: "updated_at", on: .update, format: .iso8601) var updatedAt: Date? init() {} diff --git a/Sources/DatabaseClientLive/Users+live.swift b/Sources/DatabaseClientLive/Users+live.swift index f654513..b4ec8bf 100644 --- a/Sources/DatabaseClientLive/Users+live.swift +++ b/Sources/DatabaseClientLive/Users+live.swift @@ -182,10 +182,10 @@ final class UserModel: Model, @unchecked Sendable { @Field(key: "password_hash") var passwordHash: String - @Timestamp(key: "created_at", on: .create) + @Timestamp(key: "created_at", on: .create, format: .iso8601) var createdAt: Date? - @Timestamp(key: "updated_at", on: .update) + @Timestamp(key: "updated_at", on: .update, format: .iso8601) var updatedAt: Date? @OptionalChild(for: \.$user) diff --git a/Sources/DatabaseClientLive/VendorBranches+live.swift b/Sources/DatabaseClientLive/VendorBranches+live.swift index 08a5c85..ff98dde 100644 --- a/Sources/DatabaseClientLive/VendorBranches+live.swift +++ b/Sources/DatabaseClientLive/VendorBranches+live.swift @@ -125,10 +125,10 @@ final class VendorBranchModel: Model, @unchecked Sendable { @Field(key: .string(FieldKey.name)) var name: String - @Timestamp(key: .string(FieldKey.createdAt), on: .create) + @Timestamp(key: .string(FieldKey.createdAt), on: .create, format: .iso8601) var createdAt: Date? - @Timestamp(key: .string(FieldKey.updatedAt), on: .update) + @Timestamp(key: .string(FieldKey.updatedAt), on: .update, format: .iso8601) var updatedAt: Date? @Parent(key: .string(FieldKey.vendorID)) diff --git a/Sources/DatabaseClientLive/Vendors+live.swift b/Sources/DatabaseClientLive/Vendors+live.swift index 7e58677..c2ba77d 100644 --- a/Sources/DatabaseClientLive/Vendors+live.swift +++ b/Sources/DatabaseClientLive/Vendors+live.swift @@ -109,10 +109,10 @@ final class VendorModel: Model, @unchecked Sendable { @Field(key: "name") var name: String - @Timestamp(key: "created_at", on: .create) + @Timestamp(key: "created_at", on: .create, format: .iso8601) var createdAt: Date? - @Timestamp(key: "updated_at", on: .update) + @Timestamp(key: "updated_at", on: .update, format: .iso8601) var updatedAt: Date? @Children(for: \.$vendor) diff --git a/Sources/ViewControllerLive/Dependencies/DateFormatter.swift b/Sources/ViewControllerLive/Dependencies/DateFormatter.swift index bb96c8e..943efb4 100644 --- a/Sources/ViewControllerLive/Dependencies/DateFormatter.swift +++ b/Sources/ViewControllerLive/Dependencies/DateFormatter.swift @@ -1,28 +1,53 @@ import Dependencies +import DependenciesMacros import Foundation public extension DependencyValues { - var dateFormatter: DateFormatter { - get { self[DateFormatter.self] } - set { self[DateFormatter.self] = newValue } + var dateFormatter: DateFormatterKey { + get { self[DateFormatterKey.self] } + set { self[DateFormatterKey.self] = newValue } } } -#if hasFeature(RetroactiveAttribute) - extension DateFormatter: @retroactive DependencyKey { +@DependencyClient +public struct DateFormatterKey: Sendable { + public var string: @Sendable (Date?) -> String = { _ in "N/A" } +} - public static var liveValue: DateFormatter { - let formatter = DateFormatter() - formatter.dateStyle = .short - return formatter - } +extension DateFormatterKey: DependencyKey { + public static let testValue = Self() + + public static var liveValue: DateFormatterKey { + .init( + string: { date in + guard let date else { return "N/A" } + let formatter = ISO8601DateFormatter() + formatter.timeZone = TimeZone(identifier: "UTC") + formatter.formatOptions = [.withFullDate] + return formatter.string(from: date) + } + ) } -#else - extension DateFormatter: DependencyKey { - public static var liveValue: DateFormatter { - let formatter = DateFormatter() - formatter.dateStyle = .short - return formatter - } - } -#endif +} + +// #if hasFeature(RetroactiveAttribute) +// extension DateFormatter: @retroactive DependencyKey { +// +// public static var liveValue: DateFormatter { +// let formatter = DateFormatter() +// formatter.dateStyle = .short +// return formatter +// } +// } +// #else +// extension DateFormatter: DependencyKey { +// public static var liveValue: DateFormatter { +// let formatter = ISO8601DateFormatter() +// formatter.locale = Locale(identifier: "en_US_POSIX") +// // formatter.dateFormat = "mm/dd/yyyy" +// formatter.formatOptions = [.withFullDate] +// formatter.timeZone = TimeZone(identifier: "UTC") +// return formatter +// } +// } +// #endif diff --git a/Sources/ViewControllerLive/Views/PurchaseOrders/PurchaseOrderForm.swift b/Sources/ViewControllerLive/Views/PurchaseOrders/PurchaseOrderForm.swift index 20aee58..478bbda 100644 --- a/Sources/ViewControllerLive/Views/PurchaseOrders/PurchaseOrderForm.swift +++ b/Sources/ViewControllerLive/Views/PurchaseOrders/PurchaseOrderForm.swift @@ -104,11 +104,11 @@ struct PurchaseOrderForm: HTML, Sendable { if let purchaseOrder, let createdAt = purchaseOrder.createdAt { div(.class("row")) { label(.class("label col-2")) { "Created:" } - h3(.class("col-2")) { dateFormatter.string(from: createdAt) } + h3(.class("col-2")) { dateFormatter.string(createdAt) } if let updatedAt = purchaseOrder.updatedAt { div(.class("col-1")) {} label(.class("label col-2")) { "Updated:" } - h3(.class("col-2")) { dateFormatter.string(from: updatedAt) } + h3(.class("col-2")) { dateFormatter.string(updatedAt) } } } } diff --git a/Sources/ViewControllerLive/Views/Users/UserDetail.swift b/Sources/ViewControllerLive/Views/Users/UserDetail.swift index f9d429b..1dcc009 100644 --- a/Sources/ViewControllerLive/Views/Users/UserDetail.swift +++ b/Sources/ViewControllerLive/Views/Users/UserDetail.swift @@ -25,9 +25,9 @@ struct UserDetail: HTML, Sendable { } div(.class("row")) { span(.class("label col-2")) { "Created:" } - span(.class("date col-4")) { dateFormatter.formattedDate(user.createdAt) } + span(.class("date col-4")) { dateFormatter.string(user.createdAt) } span(.class("label col-2")) { "Updated:" } - span(.class("date col-4")) { dateFormatter.formattedDate(user.updatedAt) } + span(.class("date col-4")) { dateFormatter.string(user.updatedAt) } } div(.class("btn-row user-buttons")) { button( diff --git a/Tests/ViewControllerTests/ViewControllerTests.swift b/Tests/ViewControllerTests/ViewControllerTests.swift index fc33eea..8e39879 100644 --- a/Tests/ViewControllerTests/ViewControllerTests.swift +++ b/Tests/ViewControllerTests/ViewControllerTests.swift @@ -24,6 +24,7 @@ struct ViewControllerTests { try await withDependencies { $0.viewController = .liveValue $0.database.employees = .mock + $0.dateFormatter = .mock } operation: { @Dependency(\.viewController) var viewController @Dependency(\.database) var database @@ -54,6 +55,7 @@ struct ViewControllerTests { try await withSnapshotTesting(record: record) { try await withDependencies { $0.viewController = .liveValue + $0.dateFormatter = .mock } operation: { @Dependency(\.viewController) var viewController @@ -67,7 +69,7 @@ struct ViewControllerTests { func purchaseOrderViews() async throws { try await withSnapshotTesting(record: record) { try await withDependencies { - $0.dateFormatter = .liveValue + $0.dateFormatter = .mock $0.database.purchaseOrders = .mock $0.viewController = .liveValue } operation: { @@ -103,7 +105,7 @@ struct ViewControllerTests { func userViews() async throws { try await withSnapshotTesting(record: record) { try await withDependencies { - $0.dateFormatter = .liveValue + $0.dateFormatter = .mock $0.database.users = .mock $0.viewController = .liveValue } operation: { @@ -132,7 +134,7 @@ struct ViewControllerTests { func vendorViews() async throws { try await withSnapshotTesting(record: record) { try await withDependencies { - $0.dateFormatter = .liveValue + $0.dateFormatter = .mock $0.database.vendors = .mock $0.viewController = .liveValue } operation: { @@ -161,7 +163,7 @@ struct ViewControllerTests { func vendorBranchViews() async throws { try await withSnapshotTesting(record: record) { try await withDependencies { - $0.dateFormatter = .liveValue + $0.dateFormatter = .mock $0.database.vendors = .mock $0.database.vendorBranches = .mock $0.viewController = .liveValue @@ -271,7 +273,14 @@ extension DatabaseClient.VendorBranches { extension Date { static var mock: Self { - Date(timeIntervalSince1970: 1_234_567_890) + let formatter = ISO8601DateFormatter() + return formatter.date(from: "2025-01-31T02:22:40Z")! + } +} + +extension DateFormatterKey { + static var mock: Self { + .init(string: { _ in "01/31/2025" }) } } @@ -279,10 +288,10 @@ extension Employee { static var mock: Self { Employee( id: UUID(0), - createdAt: Date(timeIntervalSince1970: 1_234_567_890), + createdAt: .mock, firstName: "Testy", lastName: "McTestface", - updatedAt: Date(timeIntervalSince1970: 1_234_567_890) + updatedAt: .mock ) } } @@ -296,10 +305,10 @@ extension Employee.Create { @Dependency(\.date.now) var now return .init( id: UUID(0), - createdAt: Date(timeIntervalSince1970: 1_234_567_890), + createdAt: .mock, firstName: firstName, lastName: lastName, - updatedAt: Date(timeIntervalSince1970: 1_234_567_890) + updatedAt: .mock ) } } diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/purchaseOrderViews.4.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/purchaseOrderViews.4.html index bb7e881..dbe2ac5 100644 --- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/purchaseOrderViews.4.html +++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/purchaseOrderViews.4.html @@ -24,10 +24,10 @@