| @@ -0,0 +1,2 @@ | |||
| .build/ | |||
| .swiftpm/ | |||
| @@ -0,0 +1,12 @@ | |||
| Packages | |||
| .build | |||
| xcuserdata | |||
| *.xcodeproj | |||
| DerivedData/ | |||
| .DS_Store | |||
| db.sqlite | |||
| .swiftpm | |||
| .env | |||
| .env.* | |||
| ! .env.example | |||
| .vscode | |||
| @@ -0,0 +1,88 @@ | |||
| # ================================ | |||
| # Build image | |||
| # ================================ | |||
| FROM swift:5.10-jammy as build | |||
| # Install OS updates | |||
| RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ | |||
| && apt-get -q update \ | |||
| && apt-get -q dist-upgrade -y \ | |||
| && apt-get install -y libjemalloc-dev | |||
| # Set up a build area | |||
| WORKDIR /build | |||
| # First just resolve dependencies. | |||
| # This creates a cached layer that can be reused | |||
| # as long as your Package.swift/Package.resolved | |||
| # files do not change. | |||
| COPY ./Package.* ./ | |||
| RUN swift package resolve --skip-update \ | |||
| $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true) | |||
| # Copy entire repo into container | |||
| COPY . . | |||
| # Build everything, with optimizations, with static linking, and using jemalloc | |||
| # N.B.: The static version of jemalloc is incompatible with the static Swift runtime. | |||
| RUN swift build -c release \ | |||
| --static-swift-stdlib \ | |||
| -Xlinker -ljemalloc | |||
| # Switch to the staging area | |||
| WORKDIR /staging | |||
| # Copy main executable to staging area | |||
| RUN cp "$(swift build --package-path /build -c release --show-bin-path)/App" ./ | |||
| # Copy static swift backtracer binary to staging area | |||
| RUN cp "/usr/libexec/swift/linux/swift-backtrace-static" ./ | |||
| # Copy resources bundled by SPM to staging area | |||
| RUN find -L "$(swift build --package-path /build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \; | |||
| # Copy any resources from the public directory and views directory if the directories exist | |||
| # Ensure that by default, neither the directory nor any of its contents are writable. | |||
| RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; } || true | |||
| RUN [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w ./Resources; } || true | |||
| # ================================ | |||
| # Run image | |||
| # ================================ | |||
| FROM ubuntu:jammy | |||
| # Make sure all system packages are up to date, and install only essential packages. | |||
| RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ | |||
| && apt-get -q update \ | |||
| && apt-get -q dist-upgrade -y \ | |||
| && apt-get -q install -y \ | |||
| libjemalloc2 \ | |||
| ca-certificates \ | |||
| tzdata \ | |||
| # If your app or its dependencies import FoundationNetworking, also install `libcurl4`. | |||
| # libcurl4 \ | |||
| # If your app or its dependencies import FoundationXML, also install `libxml2`. | |||
| # libxml2 \ | |||
| && rm -r /var/lib/apt/lists/* | |||
| # Create a vapor user and group with /app as its home directory | |||
| RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app vapor | |||
| # Switch to the new home directory | |||
| WORKDIR /app | |||
| # Copy built executable and any staged resources from builder | |||
| COPY --from=build --chown=vapor:vapor /staging /app | |||
| # Provide configuration needed by the built-in crash reporter and some sensible default behaviors. | |||
| ENV SWIFT_BACKTRACE=enable=yes,sanitize=yes,threads=all,images=all,interactive=no,swift-backtrace=./swift-backtrace-static | |||
| # Ensure all further commands run as the vapor user | |||
| USER vapor:vapor | |||
| # Let Docker bind to port 8080 | |||
| EXPOSE 8080 | |||
| # Start the Vapor service when the image is run, default to listening on 8080 in production environment | |||
| ENTRYPOINT ["./App"] | |||
| CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] | |||
| @@ -0,0 +1,41 @@ | |||
| // swift-tools-version:5.10 | |||
| import PackageDescription | |||
| let package = Package( | |||
| name: "training_v1", | |||
| platforms: [ | |||
| .macOS(.v13) | |||
| ], | |||
| dependencies: [ | |||
| // 💧 A server-side Swift web framework. | |||
| .package(url: "https://github.com/vapor/vapor.git", from: "4.92.4"), | |||
| // 🗄 An ORM for SQL and NoSQL databases. | |||
| .package(url: "https://github.com/vapor/fluent.git", from: "4.9.0"), | |||
| // 🐘 Fluent driver for Postgres. | |||
| .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.8.0"), | |||
| ], | |||
| targets: [ | |||
| .executableTarget( | |||
| name: "App", | |||
| dependencies: [ | |||
| .product(name: "Fluent", package: "fluent"), | |||
| .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"), | |||
| .product(name: "Vapor", package: "vapor"), | |||
| ], | |||
| swiftSettings: swiftSettings | |||
| ), | |||
| .testTarget( | |||
| name: "AppTests", | |||
| dependencies: [ | |||
| .target(name: "App"), | |||
| .product(name: "XCTVapor", package: "vapor"), | |||
| ], | |||
| swiftSettings: swiftSettings | |||
| ) | |||
| ] | |||
| ) | |||
| var swiftSettings: [SwiftSetting] { [ | |||
| .enableUpcomingFeature("DisableOutwardActorInference"), | |||
| .enableExperimentalFeature("StrictConcurrency"), | |||
| ] } | |||
| @@ -0,0 +1,12 @@ | |||
| <!doctype html> | |||
| <html lang="en"> | |||
| <head> | |||
| <meta charset="utf-8"> | |||
| <title>#(title)</title> | |||
| </head> | |||
| <body> | |||
| <h1>#(title)</h1> | |||
| </body> | |||
| </html> | |||
| @@ -0,0 +1,34 @@ | |||
| import Fluent | |||
| import Vapor | |||
| struct TodoController: RouteCollection { | |||
| func boot(routes: RoutesBuilder) throws { | |||
| let todos = routes.grouped("todos") | |||
| todos.get(use: { try await self.index(req: $0) }) | |||
| todos.post(use: { try await self.create(req: $0) }) | |||
| todos.group(":todoID") { todo in | |||
| todo.delete(use: { try await self.delete(req: $0) }) | |||
| } | |||
| } | |||
| func index(req: Request) async throws -> [Todo] { | |||
| try await Todo.query(on: req.db).all() | |||
| } | |||
| func create(req: Request) async throws -> Todo { | |||
| let todo = try req.content.decode(Todo.self) | |||
| try await todo.save(on: req.db) | |||
| return todo | |||
| } | |||
| func delete(req: Request) async throws -> HTTPStatus { | |||
| guard let todo = try await Todo.find(req.parameters.get("todoID"), on: req.db) else { | |||
| throw Abort(.notFound) | |||
| } | |||
| try await todo.delete(on: req.db) | |||
| return .noContent | |||
| } | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| import Fluent | |||
| struct CreateTodo: AsyncMigration { | |||
| func prepare(on database: Database) async throws { | |||
| try await database.schema("todos") | |||
| .id() | |||
| .field("title", .string, .required) | |||
| .create() | |||
| } | |||
| func revert(on database: Database) async throws { | |||
| try await database.schema("todos").delete() | |||
| } | |||
| } | |||
| @@ -0,0 +1,19 @@ | |||
| import Fluent | |||
| import Vapor | |||
| final class Todo: Model, Content { | |||
| static let schema = "todos" | |||
| @ID(key: .id) | |||
| var id: UUID? | |||
| @Field(key: "title") | |||
| var title: String | |||
| init() { } | |||
| init(id: UUID? = nil, title: String) { | |||
| self.id = id | |||
| self.title = title | |||
| } | |||
| } | |||
| @@ -0,0 +1,24 @@ | |||
| import NIOSSL | |||
| import Fluent | |||
| import FluentPostgresDriver | |||
| import Vapor | |||
| // configures your application | |||
| public func configure(_ app: Application) async throws { | |||
| // uncomment to serve files from /Public folder | |||
| // app.middleware.use(FileMiddleware(publicDirectory: app.directory.publicDirectory)) | |||
| app.databases.use(DatabaseConfigurationFactory.postgres(configuration: .init( | |||
| hostname: Environment.get("DATABASE_HOST") ?? "localhost", | |||
| port: Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? SQLPostgresConfiguration.ianaPortNumber, | |||
| username: Environment.get("DATABASE_USERNAME") ?? "vapor_username", | |||
| password: Environment.get("DATABASE_PASSWORD") ?? "vapor_password", | |||
| database: Environment.get("DATABASE_NAME") ?? "vapor_database", | |||
| tls: .prefer(try .init(configuration: .clientDefault))) | |||
| ), as: .psql) | |||
| app.migrations.add(CreateTodo()) | |||
| // register routes | |||
| try routes(app) | |||
| } | |||
| @@ -0,0 +1,21 @@ | |||
| import Vapor | |||
| import Logging | |||
| @main | |||
| enum Entrypoint { | |||
| static func main() async throws { | |||
| var env = try Environment.detect() | |||
| try LoggingSystem.bootstrap(from: &env) | |||
| let app = Application(env) | |||
| defer { app.shutdown() } | |||
| do { | |||
| try await configure(app) | |||
| } catch { | |||
| app.logger.report(error: error) | |||
| throw error | |||
| } | |||
| try await app.execute() | |||
| } | |||
| } | |||
| @@ -0,0 +1,14 @@ | |||
| import Fluent | |||
| import Vapor | |||
| func routes(_ app: Application) throws { | |||
| app.get { req async in | |||
| "It works!" | |||
| } | |||
| app.get("hello") { req async -> String in | |||
| "Hello, world!" | |||
| } | |||
| try app.register(collection: TodoController()) | |||
| } | |||
| @@ -0,0 +1,15 @@ | |||
| @testable import App | |||
| import XCTVapor | |||
| final class AppTests: XCTestCase { | |||
| func testHelloWorld() async throws { | |||
| let app = Application(.testing) | |||
| defer { app.shutdown() } | |||
| try await configure(app) | |||
| try app.test(.GET, "hello", afterResponse: { res in | |||
| XCTAssertEqual(res.status, .ok) | |||
| XCTAssertEqual(res.body.string, "Hello, world!") | |||
| }) | |||
| } | |||
| } | |||
| @@ -0,0 +1,74 @@ | |||
| # Docker Compose file for Vapor | |||
| # | |||
| # Install Docker on your system to run and test | |||
| # your Vapor app in a production-like environment. | |||
| # | |||
| # Note: This file is intended for testing and does not | |||
| # implement best practices for a production deployment. | |||
| # | |||
| # Learn more: https://docs.docker.com/compose/reference/ | |||
| # | |||
| # Build images: docker-compose build | |||
| # Start app: docker-compose up app | |||
| # Start database: docker-compose up db | |||
| # Run migrations: docker-compose run migrate | |||
| # Stop all: docker-compose down (add -v to wipe db) | |||
| # | |||
| version: '3.7' | |||
| volumes: | |||
| db_data: | |||
| x-shared_environment: &shared_environment | |||
| LOG_LEVEL: ${LOG_LEVEL:-debug} | |||
| DATABASE_HOST: db | |||
| DATABASE_NAME: vapor_database | |||
| DATABASE_USERNAME: vapor_username | |||
| DATABASE_PASSWORD: vapor_password | |||
| services: | |||
| app: | |||
| image: :latest | |||
| build: | |||
| context: . | |||
| environment: | |||
| <<: *shared_environment | |||
| depends_on: | |||
| - db | |||
| ports: | |||
| - '8080:8080' | |||
| # 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: :latest | |||
| build: | |||
| context: . | |||
| environment: | |||
| <<: *shared_environment | |||
| depends_on: | |||
| - db | |||
| command: ["migrate", "--yes"] | |||
| deploy: | |||
| replicas: 0 | |||
| revert: | |||
| image: :latest | |||
| build: | |||
| context: . | |||
| environment: | |||
| <<: *shared_environment | |||
| depends_on: | |||
| - db | |||
| command: ["migrate", "--revert", "--yes"] | |||
| deploy: | |||
| replicas: 0 | |||
| db: | |||
| image: postgres:16-alpine | |||
| volumes: | |||
| - db_data:/var/lib/postgresql/data/pgdata | |||
| environment: | |||
| PGDATA: /var/lib/postgresql/data/pgdata | |||
| POSTGRES_USER: vapor_username | |||
| POSTGRES_PASSWORD: vapor_password | |||
| POSTGRES_DB: vapor_database | |||
| ports: | |||
| - '5432:5432' | |||