| @@ -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.9-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,36 @@ | |||||
| // swift-tools-version:5.9 | |||||
| import PackageDescription | |||||
| let package = Package( | |||||
| name: "cm_services", | |||||
| platforms: [ | |||||
| .macOS(.v13) | |||||
| ], | |||||
| dependencies: [ | |||||
| // 💧 A server-side Swift web framework. | |||||
| .package(url: "https://github.com/vapor/vapor.git", from: "4.89.0"), | |||||
| // 🗄 An ORM for SQL and NoSQL databases. | |||||
| .package(url: "https://github.com/vapor/fluent.git", from: "4.8.0"), | |||||
| // 🐘 Fluent driver for Postgres. | |||||
| .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.7.2"), | |||||
| ], | |||||
| targets: [ | |||||
| .executableTarget( | |||||
| name: "App", | |||||
| dependencies: [ | |||||
| .product(name: "Fluent", package: "fluent"), | |||||
| .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"), | |||||
| .product(name: "Vapor", package: "vapor"), | |||||
| ] | |||||
| ), | |||||
| .testTarget(name: "AppTests", dependencies: [ | |||||
| .target(name: "App"), | |||||
| .product(name: "XCTVapor", package: "vapor"), | |||||
| // Workaround for https://github.com/apple/swift-package-manager/issues/6940 | |||||
| .product(name: "Vapor", package: "vapor"), | |||||
| .product(name: "Fluent", package: "Fluent"), | |||||
| .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"), | |||||
| ]) | |||||
| ] | |||||
| ) | |||||
| @@ -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,31 @@ | |||||
| import Fluent | |||||
| import Vapor | |||||
| struct TodoController: RouteCollection { | |||||
| func boot(routes: RoutesBuilder) throws { | |||||
| let todos = routes.grouped("todos") | |||||
| todos.get(use: index) | |||||
| todos.post(use: create) | |||||
| todos.group(":todoID") { todo in | |||||
| todo.delete(use: delete) | |||||
| } | |||||
| } | |||||
| 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' | |||||