Repository: tib/practical-server-side-swift Branch: main Commit: d5c447a5fefe Files: 1555 Total size: 1.3 MB Directory structure: gitextract_ovv508se/ ├── .gitignore ├── Changelog.md ├── Chapter 01/ │ └── .gitkeep ├── Chapter 02/ │ ├── SPM/ │ │ ├── .gitignore │ │ ├── Package.swift │ │ ├── README.md │ │ └── Sources/ │ │ └── main.swift │ └── VaporToolbox/ │ ├── .dockerignore │ ├── .gitignore │ ├── Dockerfile │ ├── Package.swift │ ├── Public/ │ │ └── .gitkeep │ ├── README.md │ ├── Sources/ │ │ └── myProject/ │ │ ├── Controllers/ │ │ │ └── .gitkeep │ │ ├── configure.swift │ │ ├── entrypoint.swift │ │ └── routes.swift │ ├── Tests/ │ │ └── myProjectTests/ │ │ └── myProjectTests.swift │ └── docker-compose.yml ├── Chapter 03/ │ └── myProject/ │ ├── Dockerfile │ ├── Package.swift │ ├── Public/ │ │ ├── css/ │ │ │ └── web.css │ │ └── js/ │ │ └── web.js │ ├── Sources/ │ │ └── myProject/ │ │ ├── Middlewares/ │ │ │ └── ExtendPathMiddleware.swift │ │ ├── Modules/ │ │ │ ├── Blog/ │ │ │ │ ├── BlogPost.swift │ │ │ │ ├── BlogRouter.swift │ │ │ │ ├── Controllers/ │ │ │ │ │ └── BlogFrontendController.swift │ │ │ │ └── Templates/ │ │ │ │ ├── Contexts/ │ │ │ │ │ ├── BlogPostContext.swift │ │ │ │ │ └── BlogPostsContext.swift │ │ │ │ └── Html/ │ │ │ │ ├── BlogPostTemplate.swift │ │ │ │ └── BlogPostsTemplate.swift │ │ │ └── Web/ │ │ │ ├── Controllers/ │ │ │ │ └── WebFrontendController.swift │ │ │ ├── Templates/ │ │ │ │ ├── Contexts/ │ │ │ │ │ ├── WebHomeContext.swift │ │ │ │ │ ├── WebIndexContext.swift │ │ │ │ │ └── WebLinkContext.swift │ │ │ │ └── Html/ │ │ │ │ ├── WebHomeTemplate.swift │ │ │ │ ├── WebIndexTemplate.swift │ │ │ │ └── WebLinkTemplate.swift │ │ │ └── WebRouter.swift │ │ ├── Template/ │ │ │ ├── Request+Template.swift │ │ │ ├── TemplateRenderer.swift │ │ │ └── TemplateRepresentable.swift │ │ ├── configure.swift │ │ ├── entrypoint.swift │ │ └── routes.swift │ ├── Tests/ │ │ └── myProjectTests/ │ │ └── myProjectTests.swift │ └── docker-compose.yml ├── Chapter 04/ │ └── myProject/ │ ├── Dockerfile │ ├── Package.swift │ ├── Public/ │ │ ├── css/ │ │ │ └── web.css │ │ └── js/ │ │ └── web.js │ ├── Sources/ │ │ ├── App/ │ │ │ ├── Framework/ │ │ │ │ ├── DatabaseModelInterface.swift │ │ │ │ └── ModuleInterface.swift │ │ │ ├── Middlewares/ │ │ │ │ └── ExtendPathMiddleware.swift │ │ │ ├── Modules/ │ │ │ │ ├── Blog/ │ │ │ │ │ ├── BlogModule.swift │ │ │ │ │ ├── BlogRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ └── BlogFrontendController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── BlogMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ ├── BlogCategoryModel.swift │ │ │ │ │ │ └── BlogPostModel.swift │ │ │ │ │ ├── Objects/ │ │ │ │ │ │ ├── Blog.swift │ │ │ │ │ │ ├── BlogCategory.swift │ │ │ │ │ │ └── BlogPost.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── BlogPostContext.swift │ │ │ │ │ │ └── BlogPostsContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── BlogPostTemplate.swift │ │ │ │ │ └── BlogPostsTemplate.swift │ │ │ │ └── Web/ │ │ │ │ ├── Controllers/ │ │ │ │ │ └── WebFrontendController.swift │ │ │ │ ├── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── WebHomeContext.swift │ │ │ │ │ │ ├── WebIndexContext.swift │ │ │ │ │ │ └── WebLinkContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── WebHomeTemplate.swift │ │ │ │ │ ├── WebIndexTemplate.swift │ │ │ │ │ └── WebLinkTemplate.swift │ │ │ │ ├── WebModule.swift │ │ │ │ └── WebRouter.swift │ │ │ ├── Template/ │ │ │ │ ├── Request+Template.swift │ │ │ │ ├── TemplateRenderer.swift │ │ │ │ └── TemplateRepresentable.swift │ │ │ ├── configure.swift │ │ │ └── routes.swift │ │ └── Run/ │ │ └── main.swift │ ├── Tests/ │ │ └── AppTests/ │ │ └── AppTests.swift │ └── docker-compose.yml ├── Chapter 05/ │ └── myProject/ │ ├── Dockerfile │ ├── Package.swift │ ├── Public/ │ │ ├── css/ │ │ │ └── web.css │ │ └── js/ │ │ └── web.js │ ├── Sources/ │ │ ├── App/ │ │ │ ├── Framework/ │ │ │ │ ├── AuthenticatedUser.swift │ │ │ │ ├── DatabaseModelInterface.swift │ │ │ │ └── ModuleInterface.swift │ │ │ ├── Middlewares/ │ │ │ │ └── ExtendPathMiddleware.swift │ │ │ ├── Modules/ │ │ │ │ ├── Blog/ │ │ │ │ │ ├── BlogModule.swift │ │ │ │ │ ├── BlogRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ └── BlogFrontendController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── BlogMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ ├── BlogCategoryModel.swift │ │ │ │ │ │ └── BlogPostModel.swift │ │ │ │ │ ├── Objects/ │ │ │ │ │ │ ├── Blog.swift │ │ │ │ │ │ ├── BlogCategory.swift │ │ │ │ │ │ └── BlogPost.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── BlogPostContext.swift │ │ │ │ │ │ └── BlogPostsContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── BlogPostTemplate.swift │ │ │ │ │ └── BlogPostsTemplate.swift │ │ │ │ ├── User/ │ │ │ │ │ ├── Authenticators/ │ │ │ │ │ │ ├── UserCredentialsAuthenticator.swift │ │ │ │ │ │ └── UserSessionAuthenticator.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ └── UserFrontendController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── UserMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ └── UserAccountModel.swift │ │ │ │ │ ├── Templates/ │ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ │ └── UserLoginContext.swift │ │ │ │ │ │ └── Html/ │ │ │ │ │ │ └── UserLoginTemplate.swift │ │ │ │ │ ├── UserModule.swift │ │ │ │ │ └── UserRouter.swift │ │ │ │ └── Web/ │ │ │ │ ├── Controllers/ │ │ │ │ │ └── WebFrontendController.swift │ │ │ │ ├── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── WebHomeContext.swift │ │ │ │ │ │ ├── WebIndexContext.swift │ │ │ │ │ │ └── WebLinkContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── WebHomeTemplate.swift │ │ │ │ │ ├── WebIndexTemplate.swift │ │ │ │ │ └── WebLinkTemplate.swift │ │ │ │ ├── WebModule.swift │ │ │ │ └── WebRouter.swift │ │ │ ├── Template/ │ │ │ │ ├── Request+Template.swift │ │ │ │ ├── TemplateRenderer.swift │ │ │ │ └── TemplateRepresentable.swift │ │ │ ├── configure.swift │ │ │ └── routes.swift │ │ └── Run/ │ │ └── main.swift │ ├── Tests/ │ │ └── AppTests/ │ │ └── AppTests.swift │ └── docker-compose.yml ├── Chapter 06/ │ └── myProject/ │ ├── Dockerfile │ ├── Package.swift │ ├── Public/ │ │ ├── css/ │ │ │ └── web.css │ │ └── js/ │ │ └── web.js │ ├── Sources/ │ │ ├── App/ │ │ │ ├── Framework/ │ │ │ │ ├── AuthenticatedUser.swift │ │ │ │ ├── DatabaseModelInterface.swift │ │ │ │ ├── Form/ │ │ │ │ │ ├── AbstractForm.swift │ │ │ │ │ ├── AbstractFormField.swift │ │ │ │ │ ├── Fields/ │ │ │ │ │ │ └── InputField.swift │ │ │ │ │ ├── FormAction.swift │ │ │ │ │ ├── FormComponent.swift │ │ │ │ │ ├── FormComponentBuilder.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── FormContext.swift │ │ │ │ │ │ ├── InputFieldContext.swift │ │ │ │ │ │ └── LabelContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── FormTemplate.swift │ │ │ │ │ ├── InputFieldTemplate.swift │ │ │ │ │ └── LabelTemplate.swift │ │ │ │ └── ModuleInterface.swift │ │ │ ├── Middlewares/ │ │ │ │ └── ExtendPathMiddleware.swift │ │ │ ├── Modules/ │ │ │ │ ├── Blog/ │ │ │ │ │ ├── BlogModule.swift │ │ │ │ │ ├── BlogRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ └── BlogFrontendController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── BlogMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ ├── BlogCategoryModel.swift │ │ │ │ │ │ └── BlogPostModel.swift │ │ │ │ │ ├── Objects/ │ │ │ │ │ │ ├── Blog.swift │ │ │ │ │ │ ├── BlogCategory.swift │ │ │ │ │ │ └── BlogPost.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── BlogPostContext.swift │ │ │ │ │ │ └── BlogPostsContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── BlogPostTemplate.swift │ │ │ │ │ └── BlogPostsTemplate.swift │ │ │ │ ├── User/ │ │ │ │ │ ├── Authenticators/ │ │ │ │ │ │ ├── UserCredentialsAuthenticator.swift │ │ │ │ │ │ └── UserSessionAuthenticator.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ └── UserFrontendController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── UserMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ └── UserAccountModel.swift │ │ │ │ │ ├── Forms/ │ │ │ │ │ │ └── UserLoginForm.swift │ │ │ │ │ ├── Templates/ │ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ │ └── UserLoginContext.swift │ │ │ │ │ │ └── Html/ │ │ │ │ │ │ └── UserLoginTemplate.swift │ │ │ │ │ ├── UserModule.swift │ │ │ │ │ └── UserRouter.swift │ │ │ │ └── Web/ │ │ │ │ ├── Controllers/ │ │ │ │ │ └── WebFrontendController.swift │ │ │ │ ├── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── WebHomeContext.swift │ │ │ │ │ │ ├── WebIndexContext.swift │ │ │ │ │ │ └── WebLinkContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── WebHomeTemplate.swift │ │ │ │ │ ├── WebIndexTemplate.swift │ │ │ │ │ └── WebLinkTemplate.swift │ │ │ │ ├── WebModule.swift │ │ │ │ └── WebRouter.swift │ │ │ ├── Template/ │ │ │ │ ├── Request+Template.swift │ │ │ │ ├── TemplateRenderer.swift │ │ │ │ └── TemplateRepresentable.swift │ │ │ ├── configure.swift │ │ │ └── routes.swift │ │ └── Run/ │ │ └── main.swift │ ├── Tests/ │ │ └── AppTests/ │ │ └── AppTests.swift │ └── docker-compose.yml ├── Chapter 07/ │ └── myProject/ │ ├── Dockerfile │ ├── Package.swift │ ├── Public/ │ │ ├── css/ │ │ │ └── web.css │ │ └── js/ │ │ └── web.js │ ├── Sources/ │ │ ├── App/ │ │ │ ├── Framework/ │ │ │ │ ├── AuthenticatedUser.swift │ │ │ │ ├── DatabaseModelInterface.swift │ │ │ │ ├── Form/ │ │ │ │ │ ├── AbstractForm.swift │ │ │ │ │ ├── AbstractFormField.swift │ │ │ │ │ ├── Fields/ │ │ │ │ │ │ └── InputField.swift │ │ │ │ │ ├── FormAction.swift │ │ │ │ │ ├── FormComponent.swift │ │ │ │ │ ├── FormComponentBuilder.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── FormContext.swift │ │ │ │ │ │ ├── InputFieldContext.swift │ │ │ │ │ │ └── LabelContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── FormTemplate.swift │ │ │ │ │ ├── InputFieldTemplate.swift │ │ │ │ │ └── LabelTemplate.swift │ │ │ │ ├── ModuleInterface.swift │ │ │ │ └── Validation/ │ │ │ │ ├── AsyncValidator.swift │ │ │ │ ├── AsyncValidatorBuilder.swift │ │ │ │ ├── FormFieldValidator+Validations.swift │ │ │ │ ├── FormFieldValidator.swift │ │ │ │ ├── RequestValidator.swift │ │ │ │ ├── ValidationAbort.swift │ │ │ │ └── ValidationErrorDetail.swift │ │ │ ├── Middlewares/ │ │ │ │ └── ExtendPathMiddleware.swift │ │ │ ├── Modules/ │ │ │ │ ├── Blog/ │ │ │ │ │ ├── BlogModule.swift │ │ │ │ │ ├── BlogRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ └── BlogFrontendController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── BlogMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ ├── BlogCategoryModel.swift │ │ │ │ │ │ └── BlogPostModel.swift │ │ │ │ │ ├── Objects/ │ │ │ │ │ │ ├── Blog.swift │ │ │ │ │ │ ├── BlogCategory.swift │ │ │ │ │ │ └── BlogPost.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── BlogPostContext.swift │ │ │ │ │ │ └── BlogPostsContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── BlogPostTemplate.swift │ │ │ │ │ └── BlogPostsTemplate.swift │ │ │ │ ├── User/ │ │ │ │ │ ├── Authenticators/ │ │ │ │ │ │ ├── UserCredentialsAuthenticator.swift │ │ │ │ │ │ └── UserSessionAuthenticator.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ └── UserFrontendController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── UserMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ └── UserAccountModel.swift │ │ │ │ │ ├── Forms/ │ │ │ │ │ │ └── UserLoginForm.swift │ │ │ │ │ ├── Templates/ │ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ │ └── UserLoginContext.swift │ │ │ │ │ │ └── Html/ │ │ │ │ │ │ └── UserLoginTemplate.swift │ │ │ │ │ ├── UserModule.swift │ │ │ │ │ └── UserRouter.swift │ │ │ │ └── Web/ │ │ │ │ ├── Controllers/ │ │ │ │ │ └── WebFrontendController.swift │ │ │ │ ├── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── WebHomeContext.swift │ │ │ │ │ │ ├── WebIndexContext.swift │ │ │ │ │ │ └── WebLinkContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── WebHomeTemplate.swift │ │ │ │ │ ├── WebIndexTemplate.swift │ │ │ │ │ └── WebLinkTemplate.swift │ │ │ │ ├── WebModule.swift │ │ │ │ └── WebRouter.swift │ │ │ ├── Template/ │ │ │ │ ├── Request+Template.swift │ │ │ │ ├── TemplateRenderer.swift │ │ │ │ └── TemplateRepresentable.swift │ │ │ ├── configure.swift │ │ │ └── routes.swift │ │ └── Run/ │ │ └── main.swift │ ├── Tests/ │ │ └── AppTests/ │ │ └── AppTests.swift │ └── docker-compose.yml ├── Chapter 08/ │ └── myProject/ │ ├── Dockerfile │ ├── Package.swift │ ├── Public/ │ │ ├── css/ │ │ │ └── web.css │ │ └── js/ │ │ └── web.js │ ├── Sources/ │ │ ├── App/ │ │ │ ├── Framework/ │ │ │ │ ├── AuthenticatedUser.swift │ │ │ │ ├── DatabaseModelInterface.swift │ │ │ │ ├── Extensions/ │ │ │ │ │ ├── ByteBuffer+Data.swift │ │ │ │ │ └── File+ByteBuffer.swift │ │ │ │ ├── Form/ │ │ │ │ │ ├── AbstractForm.swift │ │ │ │ │ ├── AbstractFormField.swift │ │ │ │ │ ├── Fields/ │ │ │ │ │ │ ├── HiddenField.swift │ │ │ │ │ │ ├── ImageField.swift │ │ │ │ │ │ ├── InputField.swift │ │ │ │ │ │ ├── SelectField.swift │ │ │ │ │ │ └── TextareaField.swift │ │ │ │ │ ├── FormAction.swift │ │ │ │ │ ├── FormComponent.swift │ │ │ │ │ ├── FormComponentBuilder.swift │ │ │ │ │ ├── FormImageData.swift │ │ │ │ │ ├── FormImageInput.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── FormContext.swift │ │ │ │ │ │ ├── HiddenFieldContext.swift │ │ │ │ │ │ ├── ImageFieldContext.swift │ │ │ │ │ │ ├── InputFieldContext.swift │ │ │ │ │ │ ├── LabelContext.swift │ │ │ │ │ │ ├── OptionContext.swift │ │ │ │ │ │ ├── SelectFieldContext.swift │ │ │ │ │ │ └── TextareaFieldContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── FormTemplate.swift │ │ │ │ │ ├── HiddenFieldTemplate.swift │ │ │ │ │ ├── ImageFieldTemplate.swift │ │ │ │ │ ├── InputFieldTemplate.swift │ │ │ │ │ ├── LabelTemplate.swift │ │ │ │ │ ├── SelectFieldTemplate.swift │ │ │ │ │ └── TextareaFieldTemplate.swift │ │ │ │ ├── ModuleInterface.swift │ │ │ │ └── Validation/ │ │ │ │ ├── AsyncValidator.swift │ │ │ │ ├── AsyncValidatorBuilder.swift │ │ │ │ ├── FormFieldValidator+Validations.swift │ │ │ │ ├── FormFieldValidator.swift │ │ │ │ ├── RequestValidator.swift │ │ │ │ ├── ValidationAbort.swift │ │ │ │ └── ValidationErrorDetail.swift │ │ │ ├── Middlewares/ │ │ │ │ └── ExtendPathMiddleware.swift │ │ │ ├── Modules/ │ │ │ │ ├── Blog/ │ │ │ │ │ ├── BlogModule.swift │ │ │ │ │ ├── BlogRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ └── BlogFrontendController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── BlogMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ ├── BlogCategoryModel.swift │ │ │ │ │ │ └── BlogPostModel.swift │ │ │ │ │ ├── Objects/ │ │ │ │ │ │ ├── Blog.swift │ │ │ │ │ │ ├── BlogCategory.swift │ │ │ │ │ │ └── BlogPost.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── BlogPostContext.swift │ │ │ │ │ │ └── BlogPostsContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── BlogPostTemplate.swift │ │ │ │ │ └── BlogPostsTemplate.swift │ │ │ │ ├── User/ │ │ │ │ │ ├── Authenticators/ │ │ │ │ │ │ ├── UserCredentialsAuthenticator.swift │ │ │ │ │ │ └── UserSessionAuthenticator.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ └── UserFrontendController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── UserMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ └── UserAccountModel.swift │ │ │ │ │ ├── Forms/ │ │ │ │ │ │ └── UserLoginForm.swift │ │ │ │ │ ├── Templates/ │ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ │ └── UserLoginContext.swift │ │ │ │ │ │ └── Html/ │ │ │ │ │ │ └── UserLoginTemplate.swift │ │ │ │ │ ├── UserModule.swift │ │ │ │ │ └── UserRouter.swift │ │ │ │ └── Web/ │ │ │ │ ├── Controllers/ │ │ │ │ │ └── WebFrontendController.swift │ │ │ │ ├── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── WebHomeContext.swift │ │ │ │ │ │ ├── WebIndexContext.swift │ │ │ │ │ │ └── WebLinkContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── WebHomeTemplate.swift │ │ │ │ │ ├── WebIndexTemplate.swift │ │ │ │ │ └── WebLinkTemplate.swift │ │ │ │ ├── WebModule.swift │ │ │ │ └── WebRouter.swift │ │ │ ├── Template/ │ │ │ │ ├── Request+Template.swift │ │ │ │ ├── TemplateRenderer.swift │ │ │ │ └── TemplateRepresentable.swift │ │ │ ├── configure.swift │ │ │ └── routes.swift │ │ └── Run/ │ │ └── main.swift │ ├── Tests/ │ │ └── AppTests/ │ │ └── AppTests.swift │ └── docker-compose.yml ├── Chapter 09/ │ └── myProject/ │ ├── Dockerfile │ ├── Package.swift │ ├── Public/ │ │ ├── css/ │ │ │ ├── admin.css │ │ │ └── web.css │ │ └── js/ │ │ ├── admin.js │ │ └── web.js │ ├── Sources/ │ │ ├── App/ │ │ │ ├── Extensions/ │ │ │ │ └── Svg+MenuIcon.swift │ │ │ ├── Framework/ │ │ │ │ ├── AuthenticatedUser.swift │ │ │ │ ├── DatabaseModelInterface.swift │ │ │ │ ├── Extensions/ │ │ │ │ │ ├── ByteBuffer+Data.swift │ │ │ │ │ └── File+ByteBuffer.swift │ │ │ │ ├── Form/ │ │ │ │ │ ├── AbstractForm.swift │ │ │ │ │ ├── AbstractFormField.swift │ │ │ │ │ ├── Fields/ │ │ │ │ │ │ ├── HiddenField.swift │ │ │ │ │ │ ├── ImageField.swift │ │ │ │ │ │ ├── InputField.swift │ │ │ │ │ │ ├── SelectField.swift │ │ │ │ │ │ └── TextareaField.swift │ │ │ │ │ ├── FormAction.swift │ │ │ │ │ ├── FormComponent.swift │ │ │ │ │ ├── FormComponentBuilder.swift │ │ │ │ │ ├── FormImageData.swift │ │ │ │ │ ├── FormImageInput.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── FormContext.swift │ │ │ │ │ │ ├── HiddenFieldContext.swift │ │ │ │ │ │ ├── ImageFieldContext.swift │ │ │ │ │ │ ├── InputFieldContext.swift │ │ │ │ │ │ ├── LabelContext.swift │ │ │ │ │ │ ├── OptionContext.swift │ │ │ │ │ │ ├── SelectFieldContext.swift │ │ │ │ │ │ └── TextareaFieldContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── FormTemplate.swift │ │ │ │ │ ├── HiddenFieldTemplate.swift │ │ │ │ │ ├── ImageFieldTemplate.swift │ │ │ │ │ ├── InputFieldTemplate.swift │ │ │ │ │ ├── LabelTemplate.swift │ │ │ │ │ ├── SelectFieldTemplate.swift │ │ │ │ │ └── TextareaFieldTemplate.swift │ │ │ │ ├── ModuleInterface.swift │ │ │ │ └── Validation/ │ │ │ │ ├── AsyncValidator.swift │ │ │ │ ├── AsyncValidatorBuilder.swift │ │ │ │ ├── FormFieldValidator+Validations.swift │ │ │ │ ├── FormFieldValidator.swift │ │ │ │ ├── RequestValidator.swift │ │ │ │ ├── ValidationAbort.swift │ │ │ │ └── ValidationErrorDetail.swift │ │ │ ├── Middlewares/ │ │ │ │ └── ExtendPathMiddleware.swift │ │ │ ├── Modules/ │ │ │ │ ├── Admin/ │ │ │ │ │ ├── AdminModule.swift │ │ │ │ │ ├── AdminRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ └── AdminFrontendController.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── AdminDashboardContext.swift │ │ │ │ │ │ └── AdminIndexContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── AdminDashboardTemplate.swift │ │ │ │ │ └── AdminIndexTemplate.swift │ │ │ │ ├── Blog/ │ │ │ │ │ ├── BlogModule.swift │ │ │ │ │ ├── BlogRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── BlogFrontendController.swift │ │ │ │ │ │ ├── BlogPostAdminController.swift │ │ │ │ │ │ └── BlogPostApiController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── BlogMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ ├── BlogCategoryModel.swift │ │ │ │ │ │ └── BlogPostModel.swift │ │ │ │ │ ├── Forms/ │ │ │ │ │ │ └── BlogPostEditForm.swift │ │ │ │ │ ├── Objects/ │ │ │ │ │ │ ├── Blog.swift │ │ │ │ │ │ ├── BlogCategory.swift │ │ │ │ │ │ └── BlogPost.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── BlogPostAdminDeleteContext.swift │ │ │ │ │ │ ├── BlogPostAdminDetailContext.swift │ │ │ │ │ │ ├── BlogPostAdminEditContext.swift │ │ │ │ │ │ ├── BlogPostAdminListContext.swift │ │ │ │ │ │ ├── BlogPostContext.swift │ │ │ │ │ │ └── BlogPostsContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── BlogPostAdminDeleteTemplate.swift │ │ │ │ │ ├── BlogPostAdminDetailTemplate.swift │ │ │ │ │ ├── BlogPostAdminEditTemplate.swift │ │ │ │ │ ├── BlogPostAdminListTemplate.swift │ │ │ │ │ ├── BlogPostTemplate.swift │ │ │ │ │ └── BlogPostsTemplate.swift │ │ │ │ ├── User/ │ │ │ │ │ ├── Authenticators/ │ │ │ │ │ │ ├── UserCredentialsAuthenticator.swift │ │ │ │ │ │ └── UserSessionAuthenticator.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ └── UserFrontendController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── UserMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ └── UserAccountModel.swift │ │ │ │ │ ├── Forms/ │ │ │ │ │ │ └── UserLoginForm.swift │ │ │ │ │ ├── Templates/ │ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ │ └── UserLoginContext.swift │ │ │ │ │ │ └── Html/ │ │ │ │ │ │ └── UserLoginTemplate.swift │ │ │ │ │ ├── UserModule.swift │ │ │ │ │ └── UserRouter.swift │ │ │ │ └── Web/ │ │ │ │ ├── Controllers/ │ │ │ │ │ └── WebFrontendController.swift │ │ │ │ ├── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── WebHomeContext.swift │ │ │ │ │ │ ├── WebIndexContext.swift │ │ │ │ │ │ └── WebLinkContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── WebHomeTemplate.swift │ │ │ │ │ ├── WebIndexTemplate.swift │ │ │ │ │ └── WebLinkTemplate.swift │ │ │ │ ├── WebModule.swift │ │ │ │ └── WebRouter.swift │ │ │ ├── Template/ │ │ │ │ ├── Request+Template.swift │ │ │ │ ├── TemplateRenderer.swift │ │ │ │ └── TemplateRepresentable.swift │ │ │ ├── configure.swift │ │ │ └── routes.swift │ │ └── Run/ │ │ └── main.swift │ ├── Tests/ │ │ └── AppTests/ │ │ └── AppTests.swift │ └── docker-compose.yml ├── Chapter 10/ │ └── myProject/ │ ├── Dockerfile │ ├── Package.swift │ ├── Public/ │ │ ├── css/ │ │ │ ├── admin.css │ │ │ └── web.css │ │ └── js/ │ │ ├── admin.js │ │ └── web.js │ ├── Sources/ │ │ ├── App/ │ │ │ ├── Extensions/ │ │ │ │ └── Svg+MenuIcon.swift │ │ │ ├── Framework/ │ │ │ │ ├── AuthenticatedUser.swift │ │ │ │ ├── Controllers/ │ │ │ │ │ └── ModelController.swift │ │ │ │ ├── DatabaseModelInterface.swift │ │ │ │ ├── Extensions/ │ │ │ │ │ ├── ByteBuffer+Data.swift │ │ │ │ │ └── File+ByteBuffer.swift │ │ │ │ ├── Form/ │ │ │ │ │ ├── AbstractForm.swift │ │ │ │ │ ├── AbstractFormField.swift │ │ │ │ │ ├── Fields/ │ │ │ │ │ │ ├── HiddenField.swift │ │ │ │ │ │ ├── ImageField.swift │ │ │ │ │ │ ├── InputField.swift │ │ │ │ │ │ ├── SelectField.swift │ │ │ │ │ │ └── TextareaField.swift │ │ │ │ │ ├── FormAction.swift │ │ │ │ │ ├── FormComponent.swift │ │ │ │ │ ├── FormComponentBuilder.swift │ │ │ │ │ ├── FormImageData.swift │ │ │ │ │ └── FormImageInput.swift │ │ │ │ ├── ModelEditorInterface.swift │ │ │ │ ├── ModuleInterface.swift │ │ │ │ ├── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── CellContext.swift │ │ │ │ │ │ ├── ColumnContext.swift │ │ │ │ │ │ ├── DetailContext.swift │ │ │ │ │ │ ├── FormContext.swift │ │ │ │ │ │ ├── HiddenFieldContext.swift │ │ │ │ │ │ ├── ImageFieldContext.swift │ │ │ │ │ │ ├── InputFieldContext.swift │ │ │ │ │ │ ├── LabelContext.swift │ │ │ │ │ │ ├── LinkContext.swift │ │ │ │ │ │ ├── OptionContext.swift │ │ │ │ │ │ ├── RowContext.swift │ │ │ │ │ │ ├── SelectFieldContext.swift │ │ │ │ │ │ ├── TableContext.swift │ │ │ │ │ │ └── TextareaFieldContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── CellTemplate.swift │ │ │ │ │ ├── DetailTemplate.swift │ │ │ │ │ ├── FormTemplate.swift │ │ │ │ │ ├── HiddenFieldTemplate.swift │ │ │ │ │ ├── ImageFieldTemplate.swift │ │ │ │ │ ├── InputFieldTemplate.swift │ │ │ │ │ ├── LabelTemplate.swift │ │ │ │ │ ├── LinkTemplate.swift │ │ │ │ │ ├── SelectFieldTemplate.swift │ │ │ │ │ ├── TableTemplate.swift │ │ │ │ │ └── TextareaFieldTemplate.swift │ │ │ │ └── Validation/ │ │ │ │ ├── AsyncValidator.swift │ │ │ │ ├── AsyncValidatorBuilder.swift │ │ │ │ ├── FormFieldValidator+Validations.swift │ │ │ │ ├── FormFieldValidator.swift │ │ │ │ ├── RequestValidator.swift │ │ │ │ ├── ValidationAbort.swift │ │ │ │ └── ValidationErrorDetail.swift │ │ │ ├── Middlewares/ │ │ │ │ └── ExtendPathMiddleware.swift │ │ │ ├── Modules/ │ │ │ │ ├── Admin/ │ │ │ │ │ ├── AdminModule.swift │ │ │ │ │ ├── AdminRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── AdminCreateController.swift │ │ │ │ │ │ ├── AdminDeleteController.swift │ │ │ │ │ │ ├── AdminDetailController.swift │ │ │ │ │ │ ├── AdminFrontendController.swift │ │ │ │ │ │ ├── AdminListController.swift │ │ │ │ │ │ └── AdminUpdateController.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── AdminDashboardContext.swift │ │ │ │ │ │ ├── AdminDeletePageContext.swift │ │ │ │ │ │ ├── AdminDetailPageContext.swift │ │ │ │ │ │ ├── AdminEditorPageContext.swift │ │ │ │ │ │ ├── AdminIndexContext.swift │ │ │ │ │ │ └── AdminListPageContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── AdminDashboardTemplate.swift │ │ │ │ │ ├── AdminDeletePageTemplate.swift │ │ │ │ │ ├── AdminDetailPageTemplate.swift │ │ │ │ │ ├── AdminEditorPageTemplate.swift │ │ │ │ │ ├── AdminIndexTemplate.swift │ │ │ │ │ └── AdminListPageTemplate.swift │ │ │ │ ├── Blog/ │ │ │ │ │ ├── BlogModule.swift │ │ │ │ │ ├── BlogRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── BlogCategoryAdminController.swift │ │ │ │ │ │ ├── BlogFrontendController.swift │ │ │ │ │ │ ├── BlogPostAdminController.swift │ │ │ │ │ │ └── BlogPostApiController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── BlogMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ ├── BlogCategoryModel.swift │ │ │ │ │ │ └── BlogPostModel.swift │ │ │ │ │ ├── Editors/ │ │ │ │ │ │ ├── BlogCategoryEditor.swift │ │ │ │ │ │ └── BlogPostEditor.swift │ │ │ │ │ ├── Forms/ │ │ │ │ │ │ └── BlogPostEditForm.swift │ │ │ │ │ ├── Objects/ │ │ │ │ │ │ ├── Blog.swift │ │ │ │ │ │ ├── BlogCategory.swift │ │ │ │ │ │ └── BlogPost.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── BlogPostAdminDeleteContext.swift │ │ │ │ │ │ ├── BlogPostAdminDetailContext.swift │ │ │ │ │ │ ├── BlogPostAdminEditContext.swift │ │ │ │ │ │ ├── BlogPostAdminListContext.swift │ │ │ │ │ │ ├── BlogPostContext.swift │ │ │ │ │ │ └── BlogPostsContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── BlogPostAdminDeleteTemplate.swift │ │ │ │ │ ├── BlogPostAdminDetailTemplate.swift │ │ │ │ │ ├── BlogPostAdminEditTemplate.swift │ │ │ │ │ ├── BlogPostAdminListTemplate.swift │ │ │ │ │ ├── BlogPostTemplate.swift │ │ │ │ │ └── BlogPostsTemplate.swift │ │ │ │ ├── User/ │ │ │ │ │ ├── Authenticators/ │ │ │ │ │ │ ├── UserCredentialsAuthenticator.swift │ │ │ │ │ │ └── UserSessionAuthenticator.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ └── UserFrontendController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── UserMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ └── UserAccountModel.swift │ │ │ │ │ ├── Forms/ │ │ │ │ │ │ └── UserLoginForm.swift │ │ │ │ │ ├── Templates/ │ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ │ └── UserLoginContext.swift │ │ │ │ │ │ └── Html/ │ │ │ │ │ │ └── UserLoginTemplate.swift │ │ │ │ │ ├── UserModule.swift │ │ │ │ │ └── UserRouter.swift │ │ │ │ └── Web/ │ │ │ │ ├── Controllers/ │ │ │ │ │ └── WebFrontendController.swift │ │ │ │ ├── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── WebHomeContext.swift │ │ │ │ │ │ ├── WebIndexContext.swift │ │ │ │ │ │ └── WebLinkContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── WebHomeTemplate.swift │ │ │ │ │ ├── WebIndexTemplate.swift │ │ │ │ │ └── WebLinkTemplate.swift │ │ │ │ ├── WebModule.swift │ │ │ │ └── WebRouter.swift │ │ │ ├── Template/ │ │ │ │ ├── Request+Template.swift │ │ │ │ ├── TemplateRenderer.swift │ │ │ │ └── TemplateRepresentable.swift │ │ │ ├── configure.swift │ │ │ └── routes.swift │ │ └── Run/ │ │ └── main.swift │ ├── Tests/ │ │ └── AppTests/ │ │ └── AppTests.swift │ └── docker-compose.yml ├── Chapter 11/ │ └── myProject/ │ ├── Dockerfile │ ├── Package.swift │ ├── Public/ │ │ ├── css/ │ │ │ ├── admin.css │ │ │ └── web.css │ │ └── js/ │ │ ├── admin.js │ │ └── web.js │ ├── Sources/ │ │ ├── App/ │ │ │ ├── Extensions/ │ │ │ │ └── Svg+MenuIcon.swift │ │ │ ├── Framework/ │ │ │ │ ├── AuthenticatedUser.swift │ │ │ │ ├── Controllers/ │ │ │ │ │ └── ModelController.swift │ │ │ │ ├── DatabaseModelInterface.swift │ │ │ │ ├── Extensions/ │ │ │ │ │ ├── ByteBuffer+Data.swift │ │ │ │ │ └── File+ByteBuffer.swift │ │ │ │ ├── Form/ │ │ │ │ │ ├── AbstractForm.swift │ │ │ │ │ ├── AbstractFormField.swift │ │ │ │ │ ├── Fields/ │ │ │ │ │ │ ├── HiddenField.swift │ │ │ │ │ │ ├── ImageField.swift │ │ │ │ │ │ ├── InputField.swift │ │ │ │ │ │ ├── SelectField.swift │ │ │ │ │ │ └── TextareaField.swift │ │ │ │ │ ├── FormAction.swift │ │ │ │ │ ├── FormComponent.swift │ │ │ │ │ ├── FormComponentBuilder.swift │ │ │ │ │ ├── FormImageData.swift │ │ │ │ │ └── FormImageInput.swift │ │ │ │ ├── ModelEditorInterface.swift │ │ │ │ ├── ModuleInterface.swift │ │ │ │ ├── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── CellContext.swift │ │ │ │ │ │ ├── ColumnContext.swift │ │ │ │ │ │ ├── DetailContext.swift │ │ │ │ │ │ ├── FormContext.swift │ │ │ │ │ │ ├── HiddenFieldContext.swift │ │ │ │ │ │ ├── ImageFieldContext.swift │ │ │ │ │ │ ├── InputFieldContext.swift │ │ │ │ │ │ ├── LabelContext.swift │ │ │ │ │ │ ├── LinkContext.swift │ │ │ │ │ │ ├── OptionContext.swift │ │ │ │ │ │ ├── RowContext.swift │ │ │ │ │ │ ├── SelectFieldContext.swift │ │ │ │ │ │ ├── TableContext.swift │ │ │ │ │ │ └── TextareaFieldContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── CellTemplate.swift │ │ │ │ │ ├── DetailTemplate.swift │ │ │ │ │ ├── FormTemplate.swift │ │ │ │ │ ├── HiddenFieldTemplate.swift │ │ │ │ │ ├── ImageFieldTemplate.swift │ │ │ │ │ ├── InputFieldTemplate.swift │ │ │ │ │ ├── LabelTemplate.swift │ │ │ │ │ ├── LinkTemplate.swift │ │ │ │ │ ├── SelectFieldTemplate.swift │ │ │ │ │ ├── TableTemplate.swift │ │ │ │ │ └── TextareaFieldTemplate.swift │ │ │ │ └── Validation/ │ │ │ │ ├── AsyncValidator.swift │ │ │ │ ├── AsyncValidatorBuilder.swift │ │ │ │ ├── FormFieldValidator+Validations.swift │ │ │ │ ├── FormFieldValidator.swift │ │ │ │ ├── RequestValidator.swift │ │ │ │ ├── ValidationAbort.swift │ │ │ │ └── ValidationErrorDetail.swift │ │ │ ├── Middlewares/ │ │ │ │ └── ExtendPathMiddleware.swift │ │ │ ├── Modules/ │ │ │ │ ├── Admin/ │ │ │ │ │ ├── AdminModule.swift │ │ │ │ │ ├── AdminRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── AdminCreateController.swift │ │ │ │ │ │ ├── AdminDeleteController.swift │ │ │ │ │ │ ├── AdminDetailController.swift │ │ │ │ │ │ ├── AdminFrontendController.swift │ │ │ │ │ │ ├── AdminListController.swift │ │ │ │ │ │ └── AdminUpdateController.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── AdminDashboardContext.swift │ │ │ │ │ │ ├── AdminDeletePageContext.swift │ │ │ │ │ │ ├── AdminDetailPageContext.swift │ │ │ │ │ │ ├── AdminEditorPageContext.swift │ │ │ │ │ │ ├── AdminIndexContext.swift │ │ │ │ │ │ └── AdminListPageContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── AdminDashboardTemplate.swift │ │ │ │ │ ├── AdminDeletePageTemplate.swift │ │ │ │ │ ├── AdminDetailPageTemplate.swift │ │ │ │ │ ├── AdminEditorPageTemplate.swift │ │ │ │ │ ├── AdminIndexTemplate.swift │ │ │ │ │ └── AdminListPageTemplate.swift │ │ │ │ ├── Blog/ │ │ │ │ │ ├── BlogModule.swift │ │ │ │ │ ├── BlogRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── BlogCategoryAdminController.swift │ │ │ │ │ │ ├── BlogCategoryApiController.swift │ │ │ │ │ │ ├── BlogFrontendController.swift │ │ │ │ │ │ ├── BlogPostAdminController.swift │ │ │ │ │ │ └── BlogPostApiController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── BlogMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ ├── BlogCategoryModel.swift │ │ │ │ │ │ └── BlogPostModel.swift │ │ │ │ │ ├── Editors/ │ │ │ │ │ │ ├── BlogCategoryEditor.swift │ │ │ │ │ │ └── BlogPostEditor.swift │ │ │ │ │ ├── Forms/ │ │ │ │ │ │ └── BlogPostEditForm.swift │ │ │ │ │ ├── Objects/ │ │ │ │ │ │ ├── Blog.swift │ │ │ │ │ │ ├── BlogCategory.swift │ │ │ │ │ │ └── BlogPost.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── BlogPostAdminDeleteContext.swift │ │ │ │ │ │ ├── BlogPostAdminDetailContext.swift │ │ │ │ │ │ ├── BlogPostAdminEditContext.swift │ │ │ │ │ │ ├── BlogPostAdminListContext.swift │ │ │ │ │ │ ├── BlogPostContext.swift │ │ │ │ │ │ └── BlogPostsContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── BlogPostAdminDeleteTemplate.swift │ │ │ │ │ ├── BlogPostAdminDetailTemplate.swift │ │ │ │ │ ├── BlogPostAdminEditTemplate.swift │ │ │ │ │ ├── BlogPostAdminListTemplate.swift │ │ │ │ │ ├── BlogPostTemplate.swift │ │ │ │ │ └── BlogPostsTemplate.swift │ │ │ │ ├── User/ │ │ │ │ │ ├── Authenticators/ │ │ │ │ │ │ ├── UserCredentialsAuthenticator.swift │ │ │ │ │ │ └── UserSessionAuthenticator.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ └── UserFrontendController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── UserMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ └── UserAccountModel.swift │ │ │ │ │ ├── Forms/ │ │ │ │ │ │ └── UserLoginForm.swift │ │ │ │ │ ├── Templates/ │ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ │ └── UserLoginContext.swift │ │ │ │ │ │ └── Html/ │ │ │ │ │ │ └── UserLoginTemplate.swift │ │ │ │ │ ├── UserModule.swift │ │ │ │ │ └── UserRouter.swift │ │ │ │ └── Web/ │ │ │ │ ├── Controllers/ │ │ │ │ │ └── WebFrontendController.swift │ │ │ │ ├── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── WebHomeContext.swift │ │ │ │ │ │ ├── WebIndexContext.swift │ │ │ │ │ │ └── WebLinkContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── WebHomeTemplate.swift │ │ │ │ │ ├── WebIndexTemplate.swift │ │ │ │ │ └── WebLinkTemplate.swift │ │ │ │ ├── WebModule.swift │ │ │ │ └── WebRouter.swift │ │ │ ├── Template/ │ │ │ │ ├── Request+Template.swift │ │ │ │ ├── TemplateRenderer.swift │ │ │ │ └── TemplateRepresentable.swift │ │ │ ├── configure.swift │ │ │ └── routes.swift │ │ └── Run/ │ │ └── main.swift │ ├── Tests/ │ │ └── AppTests/ │ │ └── AppTests.swift │ └── docker-compose.yml ├── Chapter 12/ │ └── myProject/ │ ├── Dockerfile │ ├── Package.swift │ ├── Public/ │ │ ├── css/ │ │ │ ├── admin.css │ │ │ └── web.css │ │ └── js/ │ │ ├── admin.js │ │ └── web.js │ ├── Sources/ │ │ ├── App/ │ │ │ ├── Extensions/ │ │ │ │ └── Svg+MenuIcon.swift │ │ │ ├── Framework/ │ │ │ │ ├── ApiModelInterface.swift │ │ │ │ ├── ApiModuleInterface.swift │ │ │ │ ├── AuthenticatedUser.swift │ │ │ │ ├── Controllers/ │ │ │ │ │ ├── CreateController.swift │ │ │ │ │ ├── DeleteController.swift │ │ │ │ │ ├── DetailController.swift │ │ │ │ │ ├── ListController.swift │ │ │ │ │ ├── ModelController.swift │ │ │ │ │ ├── PatchController.swift │ │ │ │ │ └── UpdateController.swift │ │ │ │ ├── DatabaseModelInterface.swift │ │ │ │ ├── Extensions/ │ │ │ │ │ ├── ByteBuffer+Data.swift │ │ │ │ │ └── File+ByteBuffer.swift │ │ │ │ ├── Form/ │ │ │ │ │ ├── AbstractForm.swift │ │ │ │ │ ├── AbstractFormField.swift │ │ │ │ │ ├── Fields/ │ │ │ │ │ │ ├── HiddenField.swift │ │ │ │ │ │ ├── ImageField.swift │ │ │ │ │ │ ├── InputField.swift │ │ │ │ │ │ ├── SelectField.swift │ │ │ │ │ │ └── TextareaField.swift │ │ │ │ │ ├── FormAction.swift │ │ │ │ │ ├── FormComponent.swift │ │ │ │ │ ├── FormComponentBuilder.swift │ │ │ │ │ ├── FormImageData.swift │ │ │ │ │ └── FormImageInput.swift │ │ │ │ ├── ModelEditorInterface.swift │ │ │ │ ├── ModuleInterface.swift │ │ │ │ ├── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── CellContext.swift │ │ │ │ │ │ ├── ColumnContext.swift │ │ │ │ │ │ ├── DetailContext.swift │ │ │ │ │ │ ├── FormContext.swift │ │ │ │ │ │ ├── HiddenFieldContext.swift │ │ │ │ │ │ ├── ImageFieldContext.swift │ │ │ │ │ │ ├── InputFieldContext.swift │ │ │ │ │ │ ├── LabelContext.swift │ │ │ │ │ │ ├── LinkContext.swift │ │ │ │ │ │ ├── OptionContext.swift │ │ │ │ │ │ ├── RowContext.swift │ │ │ │ │ │ ├── SelectFieldContext.swift │ │ │ │ │ │ ├── TableContext.swift │ │ │ │ │ │ └── TextareaFieldContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── CellTemplate.swift │ │ │ │ │ ├── DetailTemplate.swift │ │ │ │ │ ├── FormTemplate.swift │ │ │ │ │ ├── HiddenFieldTemplate.swift │ │ │ │ │ ├── ImageFieldTemplate.swift │ │ │ │ │ ├── InputFieldTemplate.swift │ │ │ │ │ ├── LabelTemplate.swift │ │ │ │ │ ├── LinkTemplate.swift │ │ │ │ │ ├── SelectFieldTemplate.swift │ │ │ │ │ ├── TableTemplate.swift │ │ │ │ │ └── TextareaFieldTemplate.swift │ │ │ │ └── Validation/ │ │ │ │ ├── AsyncValidator.swift │ │ │ │ ├── AsyncValidatorBuilder.swift │ │ │ │ ├── FormFieldValidator+Validations.swift │ │ │ │ ├── FormFieldValidator.swift │ │ │ │ ├── RequestValidator.swift │ │ │ │ ├── ValidationAbort.swift │ │ │ │ └── ValidationErrorDetail.swift │ │ │ ├── Middlewares/ │ │ │ │ └── ExtendPathMiddleware.swift │ │ │ ├── Modules/ │ │ │ │ ├── Admin/ │ │ │ │ │ ├── AdminModule.swift │ │ │ │ │ ├── AdminRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── AdminController.swift │ │ │ │ │ │ ├── AdminCreateController.swift │ │ │ │ │ │ ├── AdminDeleteController.swift │ │ │ │ │ │ ├── AdminDetailController.swift │ │ │ │ │ │ ├── AdminFrontendController.swift │ │ │ │ │ │ ├── AdminListController.swift │ │ │ │ │ │ └── AdminUpdateController.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── AdminDashboardContext.swift │ │ │ │ │ │ ├── AdminDeletePageContext.swift │ │ │ │ │ │ ├── AdminDetailPageContext.swift │ │ │ │ │ │ ├── AdminEditorPageContext.swift │ │ │ │ │ │ ├── AdminIndexContext.swift │ │ │ │ │ │ └── AdminListPageContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── AdminDashboardTemplate.swift │ │ │ │ │ ├── AdminDeletePageTemplate.swift │ │ │ │ │ ├── AdminDetailPageTemplate.swift │ │ │ │ │ ├── AdminEditorPageTemplate.swift │ │ │ │ │ ├── AdminIndexTemplate.swift │ │ │ │ │ └── AdminListPageTemplate.swift │ │ │ │ ├── Api/ │ │ │ │ │ └── Controllers/ │ │ │ │ │ ├── ApiController.swift │ │ │ │ │ ├── ApiCreateController.swift │ │ │ │ │ ├── ApiDeleteController.swift │ │ │ │ │ ├── ApiDetailController.swift │ │ │ │ │ ├── ApiListController.swift │ │ │ │ │ ├── ApiPatchController.swift │ │ │ │ │ └── ApiUpdateController.swift │ │ │ │ ├── Blog/ │ │ │ │ │ ├── BlogModule.swift │ │ │ │ │ ├── BlogRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── BlogCategoryAdminController.swift │ │ │ │ │ │ ├── BlogCategoryApiController.swift │ │ │ │ │ │ ├── BlogFrontendController.swift │ │ │ │ │ │ ├── BlogPostAdminController.swift │ │ │ │ │ │ └── BlogPostApiController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── BlogMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ ├── BlogCategoryModel.swift │ │ │ │ │ │ └── BlogPostModel.swift │ │ │ │ │ ├── Editors/ │ │ │ │ │ │ ├── BlogCategoryEditor.swift │ │ │ │ │ │ └── BlogPostEditor.swift │ │ │ │ │ ├── Forms/ │ │ │ │ │ │ └── BlogPostEditForm.swift │ │ │ │ │ ├── Objects/ │ │ │ │ │ │ ├── Blog.swift │ │ │ │ │ │ ├── BlogCategory.swift │ │ │ │ │ │ └── BlogPost.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── BlogPostAdminDeleteContext.swift │ │ │ │ │ │ ├── BlogPostAdminDetailContext.swift │ │ │ │ │ │ ├── BlogPostAdminEditContext.swift │ │ │ │ │ │ ├── BlogPostAdminListContext.swift │ │ │ │ │ │ ├── BlogPostContext.swift │ │ │ │ │ │ └── BlogPostsContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── BlogPostAdminDeleteTemplate.swift │ │ │ │ │ ├── BlogPostAdminDetailTemplate.swift │ │ │ │ │ ├── BlogPostAdminEditTemplate.swift │ │ │ │ │ ├── BlogPostAdminListTemplate.swift │ │ │ │ │ ├── BlogPostTemplate.swift │ │ │ │ │ └── BlogPostsTemplate.swift │ │ │ │ ├── User/ │ │ │ │ │ ├── Authenticators/ │ │ │ │ │ │ ├── UserCredentialsAuthenticator.swift │ │ │ │ │ │ └── UserSessionAuthenticator.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ └── UserFrontendController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── UserMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ └── UserAccountModel.swift │ │ │ │ │ ├── Forms/ │ │ │ │ │ │ └── UserLoginForm.swift │ │ │ │ │ ├── Templates/ │ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ │ └── UserLoginContext.swift │ │ │ │ │ │ └── Html/ │ │ │ │ │ │ └── UserLoginTemplate.swift │ │ │ │ │ ├── UserModule.swift │ │ │ │ │ └── UserRouter.swift │ │ │ │ └── Web/ │ │ │ │ ├── Controllers/ │ │ │ │ │ └── WebFrontendController.swift │ │ │ │ ├── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── WebHomeContext.swift │ │ │ │ │ │ ├── WebIndexContext.swift │ │ │ │ │ │ └── WebLinkContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── WebHomeTemplate.swift │ │ │ │ │ ├── WebIndexTemplate.swift │ │ │ │ │ └── WebLinkTemplate.swift │ │ │ │ ├── WebModule.swift │ │ │ │ └── WebRouter.swift │ │ │ ├── Template/ │ │ │ │ ├── Request+Template.swift │ │ │ │ ├── TemplateRenderer.swift │ │ │ │ └── TemplateRepresentable.swift │ │ │ ├── configure.swift │ │ │ └── routes.swift │ │ └── Run/ │ │ └── main.swift │ ├── Tests/ │ │ └── AppTests/ │ │ └── AppTests.swift │ └── docker-compose.yml ├── Chapter 13/ │ └── myProject/ │ ├── Dockerfile │ ├── Package.swift │ ├── Public/ │ │ ├── css/ │ │ │ ├── admin.css │ │ │ └── web.css │ │ └── js/ │ │ ├── admin.js │ │ └── web.js │ ├── Sources/ │ │ ├── App/ │ │ │ ├── Extensions/ │ │ │ │ └── Svg+MenuIcon.swift │ │ │ ├── Framework/ │ │ │ │ ├── ApiModelInterface.swift │ │ │ │ ├── ApiModuleInterface.swift │ │ │ │ ├── AuthenticatedUser.swift │ │ │ │ ├── Controllers/ │ │ │ │ │ ├── CreateController.swift │ │ │ │ │ ├── DeleteController.swift │ │ │ │ │ ├── DetailController.swift │ │ │ │ │ ├── ListController.swift │ │ │ │ │ ├── ModelController.swift │ │ │ │ │ ├── PatchController.swift │ │ │ │ │ └── UpdateController.swift │ │ │ │ ├── DatabaseModelInterface.swift │ │ │ │ ├── Extensions/ │ │ │ │ │ ├── ByteBuffer+Data.swift │ │ │ │ │ └── File+ByteBuffer.swift │ │ │ │ ├── Form/ │ │ │ │ │ ├── AbstractForm.swift │ │ │ │ │ ├── AbstractFormField.swift │ │ │ │ │ ├── Fields/ │ │ │ │ │ │ ├── HiddenField.swift │ │ │ │ │ │ ├── ImageField.swift │ │ │ │ │ │ ├── InputField.swift │ │ │ │ │ │ ├── SelectField.swift │ │ │ │ │ │ └── TextareaField.swift │ │ │ │ │ ├── FormAction.swift │ │ │ │ │ ├── FormComponent.swift │ │ │ │ │ ├── FormComponentBuilder.swift │ │ │ │ │ ├── FormImageData.swift │ │ │ │ │ └── FormImageInput.swift │ │ │ │ ├── ModelEditorInterface.swift │ │ │ │ ├── ModuleInterface.swift │ │ │ │ ├── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── CellContext.swift │ │ │ │ │ │ ├── ColumnContext.swift │ │ │ │ │ │ ├── DetailContext.swift │ │ │ │ │ │ ├── FormContext.swift │ │ │ │ │ │ ├── HiddenFieldContext.swift │ │ │ │ │ │ ├── ImageFieldContext.swift │ │ │ │ │ │ ├── InputFieldContext.swift │ │ │ │ │ │ ├── LabelContext.swift │ │ │ │ │ │ ├── LinkContext.swift │ │ │ │ │ │ ├── OptionContext.swift │ │ │ │ │ │ ├── RowContext.swift │ │ │ │ │ │ ├── SelectFieldContext.swift │ │ │ │ │ │ ├── TableContext.swift │ │ │ │ │ │ └── TextareaFieldContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── CellTemplate.swift │ │ │ │ │ ├── DetailTemplate.swift │ │ │ │ │ ├── FormTemplate.swift │ │ │ │ │ ├── HiddenFieldTemplate.swift │ │ │ │ │ ├── ImageFieldTemplate.swift │ │ │ │ │ ├── InputFieldTemplate.swift │ │ │ │ │ ├── LabelTemplate.swift │ │ │ │ │ ├── LinkTemplate.swift │ │ │ │ │ ├── SelectFieldTemplate.swift │ │ │ │ │ ├── TableTemplate.swift │ │ │ │ │ └── TextareaFieldTemplate.swift │ │ │ │ └── Validation/ │ │ │ │ ├── AsyncValidator.swift │ │ │ │ ├── AsyncValidatorBuilder.swift │ │ │ │ ├── FormFieldValidator+Validations.swift │ │ │ │ ├── FormFieldValidator.swift │ │ │ │ ├── KeyedContentValidator+Validations.swift │ │ │ │ ├── KeyedContentValidator.swift │ │ │ │ ├── RequestValidator.swift │ │ │ │ ├── ValidationAbort.swift │ │ │ │ ├── ValidationError.swift │ │ │ │ └── ValidationErrorDetail.swift │ │ │ ├── Middlewares/ │ │ │ │ └── ExtendPathMiddleware.swift │ │ │ ├── Modules/ │ │ │ │ ├── Admin/ │ │ │ │ │ ├── AdminModule.swift │ │ │ │ │ ├── AdminRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── AdminController.swift │ │ │ │ │ │ ├── AdminCreateController.swift │ │ │ │ │ │ ├── AdminDeleteController.swift │ │ │ │ │ │ ├── AdminDetailController.swift │ │ │ │ │ │ ├── AdminFrontendController.swift │ │ │ │ │ │ ├── AdminListController.swift │ │ │ │ │ │ └── AdminUpdateController.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── AdminDashboardContext.swift │ │ │ │ │ │ ├── AdminDeletePageContext.swift │ │ │ │ │ │ ├── AdminDetailPageContext.swift │ │ │ │ │ │ ├── AdminEditorPageContext.swift │ │ │ │ │ │ ├── AdminIndexContext.swift │ │ │ │ │ │ └── AdminListPageContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── AdminDashboardTemplate.swift │ │ │ │ │ ├── AdminDeletePageTemplate.swift │ │ │ │ │ ├── AdminDetailPageTemplate.swift │ │ │ │ │ ├── AdminEditorPageTemplate.swift │ │ │ │ │ ├── AdminIndexTemplate.swift │ │ │ │ │ └── AdminListPageTemplate.swift │ │ │ │ ├── Api/ │ │ │ │ │ ├── ApiModule.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── ApiController.swift │ │ │ │ │ │ ├── ApiCreateController.swift │ │ │ │ │ │ ├── ApiDeleteController.swift │ │ │ │ │ │ ├── ApiDetailController.swift │ │ │ │ │ │ ├── ApiListController.swift │ │ │ │ │ │ ├── ApiPatchController.swift │ │ │ │ │ │ └── ApiUpdateController.swift │ │ │ │ │ └── Middlewares/ │ │ │ │ │ └── ApiErrorMiddleware.swift │ │ │ │ ├── Blog/ │ │ │ │ │ ├── BlogModule.swift │ │ │ │ │ ├── BlogRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── BlogCategoryAdminController.swift │ │ │ │ │ │ ├── BlogCategoryApiController.swift │ │ │ │ │ │ ├── BlogFrontendController.swift │ │ │ │ │ │ ├── BlogPostAdminController.swift │ │ │ │ │ │ └── BlogPostApiController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── BlogMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ ├── BlogCategoryModel.swift │ │ │ │ │ │ └── BlogPostModel.swift │ │ │ │ │ ├── Editors/ │ │ │ │ │ │ ├── BlogCategoryEditor.swift │ │ │ │ │ │ └── BlogPostEditor.swift │ │ │ │ │ ├── Forms/ │ │ │ │ │ │ └── BlogPostEditForm.swift │ │ │ │ │ ├── Objects/ │ │ │ │ │ │ ├── Blog.swift │ │ │ │ │ │ ├── BlogCategory.swift │ │ │ │ │ │ └── BlogPost.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── BlogPostAdminDeleteContext.swift │ │ │ │ │ │ ├── BlogPostAdminDetailContext.swift │ │ │ │ │ │ ├── BlogPostAdminEditContext.swift │ │ │ │ │ │ ├── BlogPostAdminListContext.swift │ │ │ │ │ │ ├── BlogPostContext.swift │ │ │ │ │ │ └── BlogPostsContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── BlogPostAdminDeleteTemplate.swift │ │ │ │ │ ├── BlogPostAdminDetailTemplate.swift │ │ │ │ │ ├── BlogPostAdminEditTemplate.swift │ │ │ │ │ ├── BlogPostAdminListTemplate.swift │ │ │ │ │ ├── BlogPostTemplate.swift │ │ │ │ │ └── BlogPostsTemplate.swift │ │ │ │ ├── User/ │ │ │ │ │ ├── Authenticators/ │ │ │ │ │ │ ├── UserCredentialsAuthenticator.swift │ │ │ │ │ │ ├── UserSessionAuthenticator.swift │ │ │ │ │ │ └── UserTokenAuthenticator.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── UserApiController.swift │ │ │ │ │ │ └── UserFrontendController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── UserMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ ├── UserAccountModel.swift │ │ │ │ │ │ └── UserTokenModel.swift │ │ │ │ │ ├── Forms/ │ │ │ │ │ │ └── UserLoginForm.swift │ │ │ │ │ ├── Objects/ │ │ │ │ │ │ ├── User.swift │ │ │ │ │ │ ├── UserAccount.swift │ │ │ │ │ │ └── UserToken.swift │ │ │ │ │ ├── Templates/ │ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ │ └── UserLoginContext.swift │ │ │ │ │ │ └── Html/ │ │ │ │ │ │ └── UserLoginTemplate.swift │ │ │ │ │ ├── UserModule.swift │ │ │ │ │ └── UserRouter.swift │ │ │ │ └── Web/ │ │ │ │ ├── Controllers/ │ │ │ │ │ └── WebFrontendController.swift │ │ │ │ ├── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── WebHomeContext.swift │ │ │ │ │ │ ├── WebIndexContext.swift │ │ │ │ │ │ └── WebLinkContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── WebHomeTemplate.swift │ │ │ │ │ ├── WebIndexTemplate.swift │ │ │ │ │ └── WebLinkTemplate.swift │ │ │ │ ├── WebModule.swift │ │ │ │ └── WebRouter.swift │ │ │ ├── Template/ │ │ │ │ ├── Request+Template.swift │ │ │ │ ├── TemplateRenderer.swift │ │ │ │ └── TemplateRepresentable.swift │ │ │ ├── configure.swift │ │ │ └── routes.swift │ │ └── Run/ │ │ └── main.swift │ ├── Tests/ │ │ └── AppTests/ │ │ └── AppTests.swift │ └── docker-compose.yml ├── Chapter 14/ │ └── myProject/ │ ├── Dockerfile │ ├── Package.swift │ ├── Public/ │ │ ├── css/ │ │ │ ├── admin.css │ │ │ └── web.css │ │ └── js/ │ │ ├── admin.js │ │ └── web.js │ ├── Sources/ │ │ ├── App/ │ │ │ ├── Extensions/ │ │ │ │ └── Svg+MenuIcon.swift │ │ │ ├── Framework/ │ │ │ │ ├── ApiModelInterface.swift │ │ │ │ ├── ApiModuleInterface.swift │ │ │ │ ├── AuthenticatedUser.swift │ │ │ │ ├── Controllers/ │ │ │ │ │ ├── CreateController.swift │ │ │ │ │ ├── DeleteController.swift │ │ │ │ │ ├── DetailController.swift │ │ │ │ │ ├── ListController.swift │ │ │ │ │ ├── ModelController.swift │ │ │ │ │ ├── PatchController.swift │ │ │ │ │ └── UpdateController.swift │ │ │ │ ├── DatabaseModelInterface.swift │ │ │ │ ├── Extensions/ │ │ │ │ │ ├── ByteBuffer+Data.swift │ │ │ │ │ └── File+ByteBuffer.swift │ │ │ │ ├── Form/ │ │ │ │ │ ├── AbstractForm.swift │ │ │ │ │ ├── AbstractFormField.swift │ │ │ │ │ ├── Fields/ │ │ │ │ │ │ ├── HiddenField.swift │ │ │ │ │ │ ├── ImageField.swift │ │ │ │ │ │ ├── InputField.swift │ │ │ │ │ │ ├── SelectField.swift │ │ │ │ │ │ └── TextareaField.swift │ │ │ │ │ ├── FormAction.swift │ │ │ │ │ ├── FormComponent.swift │ │ │ │ │ ├── FormComponentBuilder.swift │ │ │ │ │ ├── FormImageData.swift │ │ │ │ │ └── FormImageInput.swift │ │ │ │ ├── ModelEditorInterface.swift │ │ │ │ ├── ModuleInterface.swift │ │ │ │ ├── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── CellContext.swift │ │ │ │ │ │ ├── ColumnContext.swift │ │ │ │ │ │ ├── DetailContext.swift │ │ │ │ │ │ ├── FormContext.swift │ │ │ │ │ │ ├── HiddenFieldContext.swift │ │ │ │ │ │ ├── ImageFieldContext.swift │ │ │ │ │ │ ├── InputFieldContext.swift │ │ │ │ │ │ ├── LabelContext.swift │ │ │ │ │ │ ├── LinkContext.swift │ │ │ │ │ │ ├── OptionContext.swift │ │ │ │ │ │ ├── RowContext.swift │ │ │ │ │ │ ├── SelectFieldContext.swift │ │ │ │ │ │ ├── TableContext.swift │ │ │ │ │ │ └── TextareaFieldContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── CellTemplate.swift │ │ │ │ │ ├── DetailTemplate.swift │ │ │ │ │ ├── FormTemplate.swift │ │ │ │ │ ├── HiddenFieldTemplate.swift │ │ │ │ │ ├── ImageFieldTemplate.swift │ │ │ │ │ ├── InputFieldTemplate.swift │ │ │ │ │ ├── LabelTemplate.swift │ │ │ │ │ ├── LinkTemplate.swift │ │ │ │ │ ├── SelectFieldTemplate.swift │ │ │ │ │ ├── TableTemplate.swift │ │ │ │ │ └── TextareaFieldTemplate.swift │ │ │ │ └── Validation/ │ │ │ │ ├── AsyncValidator.swift │ │ │ │ ├── AsyncValidatorBuilder.swift │ │ │ │ ├── FormFieldValidator+Validations.swift │ │ │ │ ├── FormFieldValidator.swift │ │ │ │ ├── KeyedContentValidator+Validations.swift │ │ │ │ ├── KeyedContentValidator.swift │ │ │ │ ├── RequestValidator.swift │ │ │ │ ├── ValidationAbort.swift │ │ │ │ ├── ValidationError.swift │ │ │ │ └── ValidationErrorDetail.swift │ │ │ ├── Middlewares/ │ │ │ │ └── ExtendPathMiddleware.swift │ │ │ ├── Modules/ │ │ │ │ ├── Admin/ │ │ │ │ │ ├── AdminModule.swift │ │ │ │ │ ├── AdminRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── AdminController.swift │ │ │ │ │ │ ├── AdminCreateController.swift │ │ │ │ │ │ ├── AdminDeleteController.swift │ │ │ │ │ │ ├── AdminDetailController.swift │ │ │ │ │ │ ├── AdminFrontendController.swift │ │ │ │ │ │ ├── AdminListController.swift │ │ │ │ │ │ └── AdminUpdateController.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── AdminDashboardContext.swift │ │ │ │ │ │ ├── AdminDeletePageContext.swift │ │ │ │ │ │ ├── AdminDetailPageContext.swift │ │ │ │ │ │ ├── AdminEditorPageContext.swift │ │ │ │ │ │ ├── AdminIndexContext.swift │ │ │ │ │ │ └── AdminListPageContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── AdminDashboardTemplate.swift │ │ │ │ │ ├── AdminDeletePageTemplate.swift │ │ │ │ │ ├── AdminDetailPageTemplate.swift │ │ │ │ │ ├── AdminEditorPageTemplate.swift │ │ │ │ │ ├── AdminIndexTemplate.swift │ │ │ │ │ └── AdminListPageTemplate.swift │ │ │ │ ├── Api/ │ │ │ │ │ ├── ApiModule.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── ApiController.swift │ │ │ │ │ │ ├── ApiCreateController.swift │ │ │ │ │ │ ├── ApiDeleteController.swift │ │ │ │ │ │ ├── ApiDetailController.swift │ │ │ │ │ │ ├── ApiListController.swift │ │ │ │ │ │ ├── ApiPatchController.swift │ │ │ │ │ │ └── ApiUpdateController.swift │ │ │ │ │ └── Middlewares/ │ │ │ │ │ └── ApiErrorMiddleware.swift │ │ │ │ ├── Blog/ │ │ │ │ │ ├── BlogModule.swift │ │ │ │ │ ├── BlogRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── BlogCategoryAdminController.swift │ │ │ │ │ │ ├── BlogCategoryApiController.swift │ │ │ │ │ │ ├── BlogFrontendController.swift │ │ │ │ │ │ ├── BlogPostAdminController.swift │ │ │ │ │ │ └── BlogPostApiController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── BlogMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ ├── BlogCategoryModel.swift │ │ │ │ │ │ └── BlogPostModel.swift │ │ │ │ │ ├── Editors/ │ │ │ │ │ │ ├── BlogCategoryEditor.swift │ │ │ │ │ │ └── BlogPostEditor.swift │ │ │ │ │ ├── Forms/ │ │ │ │ │ │ └── BlogPostEditForm.swift │ │ │ │ │ ├── Objects/ │ │ │ │ │ │ ├── Blog.swift │ │ │ │ │ │ ├── BlogCategory.swift │ │ │ │ │ │ └── BlogPost.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── BlogPostAdminDeleteContext.swift │ │ │ │ │ │ ├── BlogPostAdminDetailContext.swift │ │ │ │ │ │ ├── BlogPostAdminEditContext.swift │ │ │ │ │ │ ├── BlogPostAdminListContext.swift │ │ │ │ │ │ ├── BlogPostContext.swift │ │ │ │ │ │ └── BlogPostsContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── BlogPostAdminDeleteTemplate.swift │ │ │ │ │ ├── BlogPostAdminDetailTemplate.swift │ │ │ │ │ ├── BlogPostAdminEditTemplate.swift │ │ │ │ │ ├── BlogPostAdminListTemplate.swift │ │ │ │ │ ├── BlogPostTemplate.swift │ │ │ │ │ └── BlogPostsTemplate.swift │ │ │ │ ├── User/ │ │ │ │ │ ├── Authenticators/ │ │ │ │ │ │ ├── UserCredentialsAuthenticator.swift │ │ │ │ │ │ ├── UserSessionAuthenticator.swift │ │ │ │ │ │ └── UserTokenAuthenticator.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── UserApiController.swift │ │ │ │ │ │ └── UserFrontendController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── UserMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ ├── UserAccountModel.swift │ │ │ │ │ │ └── UserTokenModel.swift │ │ │ │ │ ├── Forms/ │ │ │ │ │ │ └── UserLoginForm.swift │ │ │ │ │ ├── Objects/ │ │ │ │ │ │ ├── User.swift │ │ │ │ │ │ ├── UserAccount.swift │ │ │ │ │ │ └── UserToken.swift │ │ │ │ │ ├── Templates/ │ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ │ └── UserLoginContext.swift │ │ │ │ │ │ └── Html/ │ │ │ │ │ │ └── UserLoginTemplate.swift │ │ │ │ │ ├── UserModule.swift │ │ │ │ │ └── UserRouter.swift │ │ │ │ └── Web/ │ │ │ │ ├── Controllers/ │ │ │ │ │ └── WebFrontendController.swift │ │ │ │ ├── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── WebHomeContext.swift │ │ │ │ │ │ ├── WebIndexContext.swift │ │ │ │ │ │ └── WebLinkContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── WebHomeTemplate.swift │ │ │ │ │ ├── WebIndexTemplate.swift │ │ │ │ │ └── WebLinkTemplate.swift │ │ │ │ ├── WebModule.swift │ │ │ │ └── WebRouter.swift │ │ │ ├── Template/ │ │ │ │ ├── Request+Template.swift │ │ │ │ ├── TemplateRenderer.swift │ │ │ │ └── TemplateRepresentable.swift │ │ │ ├── configure.swift │ │ │ └── routes.swift │ │ └── Run/ │ │ └── main.swift │ ├── Tests/ │ │ └── AppTests/ │ │ ├── AppTests.swift │ │ ├── BlogCategoryApiTests.swift │ │ ├── BlogPostApiTests.swift │ │ └── Framework/ │ │ ├── AppTestCase.swift │ │ └── XCTApplicationTester.swift │ └── docker-compose.yml ├── Chapter 15/ │ └── myProject/ │ ├── Dockerfile │ ├── Package.swift │ ├── Public/ │ │ ├── css/ │ │ │ ├── admin.css │ │ │ └── web.css │ │ └── js/ │ │ ├── admin.js │ │ └── web.js │ ├── Sources/ │ │ ├── App/ │ │ │ ├── Extensions/ │ │ │ │ └── Svg+MenuIcon.swift │ │ │ ├── Framework/ │ │ │ │ ├── ApiModelInterface.swift │ │ │ │ ├── ApiModuleInterface.swift │ │ │ │ ├── AuthenticatedUser.swift │ │ │ │ ├── Controllers/ │ │ │ │ │ ├── CreateController.swift │ │ │ │ │ ├── DeleteController.swift │ │ │ │ │ ├── DetailController.swift │ │ │ │ │ ├── ListController.swift │ │ │ │ │ ├── ModelController.swift │ │ │ │ │ ├── PatchController.swift │ │ │ │ │ └── UpdateController.swift │ │ │ │ ├── DatabaseModelInterface.swift │ │ │ │ ├── Extensions/ │ │ │ │ │ ├── ByteBuffer+Data.swift │ │ │ │ │ └── File+ByteBuffer.swift │ │ │ │ ├── Form/ │ │ │ │ │ ├── AbstractForm.swift │ │ │ │ │ ├── AbstractFormField.swift │ │ │ │ │ ├── Fields/ │ │ │ │ │ │ ├── HiddenField.swift │ │ │ │ │ │ ├── ImageField.swift │ │ │ │ │ │ ├── InputField.swift │ │ │ │ │ │ ├── SelectField.swift │ │ │ │ │ │ └── TextareaField.swift │ │ │ │ │ ├── FormAction.swift │ │ │ │ │ ├── FormComponent.swift │ │ │ │ │ ├── FormComponentBuilder.swift │ │ │ │ │ ├── FormImageData.swift │ │ │ │ │ └── FormImageInput.swift │ │ │ │ ├── Hooks/ │ │ │ │ │ ├── Application+HookStorage.swift │ │ │ │ │ ├── Async/ │ │ │ │ │ │ ├── Application+AsyncHooks.swift │ │ │ │ │ │ ├── AsyncAnyHookFunction.swift │ │ │ │ │ │ ├── AsyncHookFunction.swift │ │ │ │ │ │ ├── HookStorage+AsyncHooks.swift │ │ │ │ │ │ └── Request+AsyncHooks.swift │ │ │ │ │ ├── HookArguments.swift │ │ │ │ │ ├── HookFunctionPointer.swift │ │ │ │ │ ├── HookStorage.swift │ │ │ │ │ └── Sync/ │ │ │ │ │ ├── AnyHookFunction.swift │ │ │ │ │ ├── Application+Hooks.swift │ │ │ │ │ ├── HookFunction.swift │ │ │ │ │ ├── HookStorage+Hooks.swift │ │ │ │ │ └── Request+Hooks.swift │ │ │ │ ├── ModelEditorInterface.swift │ │ │ │ ├── ModuleInterface.swift │ │ │ │ ├── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── CellContext.swift │ │ │ │ │ │ ├── ColumnContext.swift │ │ │ │ │ │ ├── DetailContext.swift │ │ │ │ │ │ ├── FormContext.swift │ │ │ │ │ │ ├── HiddenFieldContext.swift │ │ │ │ │ │ ├── ImageFieldContext.swift │ │ │ │ │ │ ├── InputFieldContext.swift │ │ │ │ │ │ ├── LabelContext.swift │ │ │ │ │ │ ├── LinkContext.swift │ │ │ │ │ │ ├── OptionContext.swift │ │ │ │ │ │ ├── RowContext.swift │ │ │ │ │ │ ├── SelectFieldContext.swift │ │ │ │ │ │ ├── TableContext.swift │ │ │ │ │ │ └── TextareaFieldContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── CellTemplate.swift │ │ │ │ │ ├── DetailTemplate.swift │ │ │ │ │ ├── FormTemplate.swift │ │ │ │ │ ├── HiddenFieldTemplate.swift │ │ │ │ │ ├── ImageFieldTemplate.swift │ │ │ │ │ ├── InputFieldTemplate.swift │ │ │ │ │ ├── LabelTemplate.swift │ │ │ │ │ ├── LinkTemplate.swift │ │ │ │ │ ├── SelectFieldTemplate.swift │ │ │ │ │ ├── TableTemplate.swift │ │ │ │ │ └── TextareaFieldTemplate.swift │ │ │ │ └── Validation/ │ │ │ │ ├── AsyncValidator.swift │ │ │ │ ├── AsyncValidatorBuilder.swift │ │ │ │ ├── FormFieldValidator+Validations.swift │ │ │ │ ├── FormFieldValidator.swift │ │ │ │ ├── KeyedContentValidator+Validations.swift │ │ │ │ ├── KeyedContentValidator.swift │ │ │ │ ├── RequestValidator.swift │ │ │ │ ├── ValidationAbort.swift │ │ │ │ ├── ValidationError.swift │ │ │ │ └── ValidationErrorDetail.swift │ │ │ ├── Middlewares/ │ │ │ │ └── ExtendPathMiddleware.swift │ │ │ ├── Modules/ │ │ │ │ ├── Admin/ │ │ │ │ │ ├── AdminModule.swift │ │ │ │ │ ├── AdminRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── AdminController.swift │ │ │ │ │ │ ├── AdminCreateController.swift │ │ │ │ │ │ ├── AdminDeleteController.swift │ │ │ │ │ │ ├── AdminDetailController.swift │ │ │ │ │ │ ├── AdminFrontendController.swift │ │ │ │ │ │ ├── AdminListController.swift │ │ │ │ │ │ └── AdminUpdateController.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── AdminDashboardContext.swift │ │ │ │ │ │ ├── AdminDeletePageContext.swift │ │ │ │ │ │ ├── AdminDetailPageContext.swift │ │ │ │ │ │ ├── AdminEditorPageContext.swift │ │ │ │ │ │ ├── AdminIndexContext.swift │ │ │ │ │ │ └── AdminListPageContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── AdminDashboardTemplate.swift │ │ │ │ │ ├── AdminDeletePageTemplate.swift │ │ │ │ │ ├── AdminDetailPageTemplate.swift │ │ │ │ │ ├── AdminEditorPageTemplate.swift │ │ │ │ │ ├── AdminIndexTemplate.swift │ │ │ │ │ └── AdminListPageTemplate.swift │ │ │ │ ├── Api/ │ │ │ │ │ ├── ApiModule.swift │ │ │ │ │ ├── ApiRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── ApiController.swift │ │ │ │ │ │ ├── ApiCreateController.swift │ │ │ │ │ │ ├── ApiDeleteController.swift │ │ │ │ │ │ ├── ApiDetailController.swift │ │ │ │ │ │ ├── ApiListController.swift │ │ │ │ │ │ ├── ApiPatchController.swift │ │ │ │ │ │ └── ApiUpdateController.swift │ │ │ │ │ └── Middlewares/ │ │ │ │ │ └── ApiErrorMiddleware.swift │ │ │ │ ├── Blog/ │ │ │ │ │ ├── BlogModule.swift │ │ │ │ │ ├── BlogRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── BlogCategoryAdminController.swift │ │ │ │ │ │ ├── BlogCategoryApiController.swift │ │ │ │ │ │ ├── BlogFrontendController.swift │ │ │ │ │ │ ├── BlogPostAdminController.swift │ │ │ │ │ │ └── BlogPostApiController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── BlogMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ ├── BlogCategoryModel.swift │ │ │ │ │ │ └── BlogPostModel.swift │ │ │ │ │ ├── Editors/ │ │ │ │ │ │ ├── BlogCategoryEditor.swift │ │ │ │ │ │ └── BlogPostEditor.swift │ │ │ │ │ ├── Forms/ │ │ │ │ │ │ └── BlogPostEditForm.swift │ │ │ │ │ ├── Objects/ │ │ │ │ │ │ ├── Blog.swift │ │ │ │ │ │ ├── BlogCategory.swift │ │ │ │ │ │ └── BlogPost.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── BlogPostAdminDeleteContext.swift │ │ │ │ │ │ ├── BlogPostAdminDetailContext.swift │ │ │ │ │ │ ├── BlogPostAdminEditContext.swift │ │ │ │ │ │ ├── BlogPostAdminListContext.swift │ │ │ │ │ │ ├── BlogPostContext.swift │ │ │ │ │ │ └── BlogPostsContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── BlogAdminWidgetTemplate.swift │ │ │ │ │ ├── BlogPostAdminDeleteTemplate.swift │ │ │ │ │ ├── BlogPostAdminDetailTemplate.swift │ │ │ │ │ ├── BlogPostAdminEditTemplate.swift │ │ │ │ │ ├── BlogPostAdminListTemplate.swift │ │ │ │ │ ├── BlogPostTemplate.swift │ │ │ │ │ └── BlogPostsTemplate.swift │ │ │ │ ├── User/ │ │ │ │ │ ├── Authenticators/ │ │ │ │ │ │ ├── UserCredentialsAuthenticator.swift │ │ │ │ │ │ ├── UserSessionAuthenticator.swift │ │ │ │ │ │ └── UserTokenAuthenticator.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── UserApiController.swift │ │ │ │ │ │ └── UserFrontendController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── UserMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ ├── UserAccountModel.swift │ │ │ │ │ │ └── UserTokenModel.swift │ │ │ │ │ ├── Forms/ │ │ │ │ │ │ └── UserLoginForm.swift │ │ │ │ │ ├── Objects/ │ │ │ │ │ │ ├── User.swift │ │ │ │ │ │ ├── UserAccount.swift │ │ │ │ │ │ └── UserToken.swift │ │ │ │ │ ├── Templates/ │ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ │ └── UserLoginContext.swift │ │ │ │ │ │ └── Html/ │ │ │ │ │ │ └── UserLoginTemplate.swift │ │ │ │ │ ├── UserModule.swift │ │ │ │ │ └── UserRouter.swift │ │ │ │ └── Web/ │ │ │ │ ├── Controllers/ │ │ │ │ │ └── WebFrontendController.swift │ │ │ │ ├── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── WebHomeContext.swift │ │ │ │ │ │ ├── WebIndexContext.swift │ │ │ │ │ │ └── WebLinkContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── WebHomeTemplate.swift │ │ │ │ │ ├── WebIndexTemplate.swift │ │ │ │ │ └── WebLinkTemplate.swift │ │ │ │ ├── WebModule.swift │ │ │ │ └── WebRouter.swift │ │ │ ├── Template/ │ │ │ │ ├── Request+Template.swift │ │ │ │ ├── TemplateRenderer.swift │ │ │ │ └── TemplateRepresentable.swift │ │ │ ├── configure.swift │ │ │ └── routes.swift │ │ └── Run/ │ │ └── main.swift │ ├── Tests/ │ │ └── AppTests/ │ │ ├── AppTests.swift │ │ ├── BlogCategoryApiTests.swift │ │ ├── BlogPostApiTests.swift │ │ └── Framework/ │ │ ├── AppTestCase.swift │ │ └── XCTApplicationTester.swift │ └── docker-compose.yml ├── Chapter 16/ │ └── myProject/ │ ├── Dockerfile │ ├── Package.swift │ ├── Public/ │ │ ├── css/ │ │ │ ├── admin.css │ │ │ └── web.css │ │ └── js/ │ │ ├── admin.js │ │ └── web.js │ ├── Sources/ │ │ ├── App/ │ │ │ ├── Extensions/ │ │ │ │ └── Svg+MenuIcon.swift │ │ │ ├── Framework/ │ │ │ │ ├── ApiModelInterface+PathComponent.swift │ │ │ │ ├── AuthenticatedUser.swift │ │ │ │ ├── Controllers/ │ │ │ │ │ ├── CreateController.swift │ │ │ │ │ ├── DeleteController.swift │ │ │ │ │ ├── DetailController.swift │ │ │ │ │ ├── ListController.swift │ │ │ │ │ ├── ModelController.swift │ │ │ │ │ ├── PatchController.swift │ │ │ │ │ └── UpdateController.swift │ │ │ │ ├── DatabaseModelInterface.swift │ │ │ │ ├── Extensions/ │ │ │ │ │ ├── ByteBuffer+Data.swift │ │ │ │ │ └── File+ByteBuffer.swift │ │ │ │ ├── Form/ │ │ │ │ │ ├── AbstractForm.swift │ │ │ │ │ ├── AbstractFormField.swift │ │ │ │ │ ├── Fields/ │ │ │ │ │ │ ├── HiddenField.swift │ │ │ │ │ │ ├── ImageField.swift │ │ │ │ │ │ ├── InputField.swift │ │ │ │ │ │ ├── SelectField.swift │ │ │ │ │ │ └── TextareaField.swift │ │ │ │ │ ├── FormAction.swift │ │ │ │ │ ├── FormComponent.swift │ │ │ │ │ ├── FormComponentBuilder.swift │ │ │ │ │ ├── FormImageData.swift │ │ │ │ │ └── FormImageInput.swift │ │ │ │ ├── Hooks/ │ │ │ │ │ ├── Application+HookStorage.swift │ │ │ │ │ ├── Async/ │ │ │ │ │ │ ├── Application+AsyncHooks.swift │ │ │ │ │ │ ├── AsyncAnyHookFunction.swift │ │ │ │ │ │ ├── AsyncHookFunction.swift │ │ │ │ │ │ ├── HookStorage+AsyncHooks.swift │ │ │ │ │ │ └── Request+AsyncHooks.swift │ │ │ │ │ ├── HookArguments.swift │ │ │ │ │ ├── HookFunctionPointer.swift │ │ │ │ │ ├── HookStorage.swift │ │ │ │ │ └── Sync/ │ │ │ │ │ ├── AnyHookFunction.swift │ │ │ │ │ ├── Application+Hooks.swift │ │ │ │ │ ├── HookFunction.swift │ │ │ │ │ ├── HookStorage+Hooks.swift │ │ │ │ │ └── Request+Hooks.swift │ │ │ │ ├── ModelEditorInterface.swift │ │ │ │ ├── ModuleInterface.swift │ │ │ │ ├── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── CellContext.swift │ │ │ │ │ │ ├── ColumnContext.swift │ │ │ │ │ │ ├── DetailContext.swift │ │ │ │ │ │ ├── FormContext.swift │ │ │ │ │ │ ├── HiddenFieldContext.swift │ │ │ │ │ │ ├── ImageFieldContext.swift │ │ │ │ │ │ ├── InputFieldContext.swift │ │ │ │ │ │ ├── LabelContext.swift │ │ │ │ │ │ ├── LinkContext.swift │ │ │ │ │ │ ├── OptionContext.swift │ │ │ │ │ │ ├── RowContext.swift │ │ │ │ │ │ ├── SelectFieldContext.swift │ │ │ │ │ │ ├── TableContext.swift │ │ │ │ │ │ └── TextareaFieldContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── CellTemplate.swift │ │ │ │ │ ├── DetailTemplate.swift │ │ │ │ │ ├── FormTemplate.swift │ │ │ │ │ ├── HiddenFieldTemplate.swift │ │ │ │ │ ├── ImageFieldTemplate.swift │ │ │ │ │ ├── InputFieldTemplate.swift │ │ │ │ │ ├── LabelTemplate.swift │ │ │ │ │ ├── LinkTemplate.swift │ │ │ │ │ ├── SelectFieldTemplate.swift │ │ │ │ │ ├── TableTemplate.swift │ │ │ │ │ └── TextareaFieldTemplate.swift │ │ │ │ └── Validation/ │ │ │ │ ├── AsyncValidator.swift │ │ │ │ ├── AsyncValidatorBuilder.swift │ │ │ │ ├── FormFieldValidator+Validations.swift │ │ │ │ ├── FormFieldValidator.swift │ │ │ │ ├── KeyedContentValidator+Validations.swift │ │ │ │ ├── KeyedContentValidator.swift │ │ │ │ ├── RequestValidator.swift │ │ │ │ ├── ValidationAbort.swift │ │ │ │ ├── ValidationError.swift │ │ │ │ └── ValidationErrorDetail.swift │ │ │ ├── Middlewares/ │ │ │ │ └── ExtendPathMiddleware.swift │ │ │ ├── Modules/ │ │ │ │ ├── Admin/ │ │ │ │ │ ├── AdminModule.swift │ │ │ │ │ ├── AdminRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── AdminController.swift │ │ │ │ │ │ ├── AdminCreateController.swift │ │ │ │ │ │ ├── AdminDeleteController.swift │ │ │ │ │ │ ├── AdminDetailController.swift │ │ │ │ │ │ ├── AdminFrontendController.swift │ │ │ │ │ │ ├── AdminListController.swift │ │ │ │ │ │ └── AdminUpdateController.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── AdminDashboardContext.swift │ │ │ │ │ │ ├── AdminDeletePageContext.swift │ │ │ │ │ │ ├── AdminDetailPageContext.swift │ │ │ │ │ │ ├── AdminEditorPageContext.swift │ │ │ │ │ │ ├── AdminIndexContext.swift │ │ │ │ │ │ └── AdminListPageContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── AdminDashboardTemplate.swift │ │ │ │ │ ├── AdminDeletePageTemplate.swift │ │ │ │ │ ├── AdminDetailPageTemplate.swift │ │ │ │ │ ├── AdminEditorPageTemplate.swift │ │ │ │ │ ├── AdminIndexTemplate.swift │ │ │ │ │ └── AdminListPageTemplate.swift │ │ │ │ ├── Api/ │ │ │ │ │ ├── ApiModule.swift │ │ │ │ │ ├── ApiRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── ApiController.swift │ │ │ │ │ │ ├── ApiCreateController.swift │ │ │ │ │ │ ├── ApiDeleteController.swift │ │ │ │ │ │ ├── ApiDetailController.swift │ │ │ │ │ │ ├── ApiListController.swift │ │ │ │ │ │ ├── ApiPatchController.swift │ │ │ │ │ │ └── ApiUpdateController.swift │ │ │ │ │ └── Middlewares/ │ │ │ │ │ └── ApiErrorMiddleware.swift │ │ │ │ ├── Blog/ │ │ │ │ │ ├── BlogModule.swift │ │ │ │ │ ├── BlogRouter.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── BlogCategoryAdminController.swift │ │ │ │ │ │ ├── BlogCategoryApiController.swift │ │ │ │ │ │ ├── BlogFrontendController.swift │ │ │ │ │ │ ├── BlogPostAdminController.swift │ │ │ │ │ │ └── BlogPostApiController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── BlogMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ ├── BlogCategoryModel.swift │ │ │ │ │ │ └── BlogPostModel.swift │ │ │ │ │ ├── Editors/ │ │ │ │ │ │ ├── BlogCategoryEditor.swift │ │ │ │ │ │ └── BlogPostEditor.swift │ │ │ │ │ ├── Forms/ │ │ │ │ │ │ └── BlogPostEditForm.swift │ │ │ │ │ └── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── BlogPostAdminDeleteContext.swift │ │ │ │ │ │ ├── BlogPostAdminDetailContext.swift │ │ │ │ │ │ ├── BlogPostAdminEditContext.swift │ │ │ │ │ │ ├── BlogPostAdminListContext.swift │ │ │ │ │ │ ├── BlogPostContext.swift │ │ │ │ │ │ └── BlogPostsContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── BlogAdminWidgetTemplate.swift │ │ │ │ │ ├── BlogPostAdminDeleteTemplate.swift │ │ │ │ │ ├── BlogPostAdminDetailTemplate.swift │ │ │ │ │ ├── BlogPostAdminEditTemplate.swift │ │ │ │ │ ├── BlogPostAdminListTemplate.swift │ │ │ │ │ ├── BlogPostTemplate.swift │ │ │ │ │ └── BlogPostsTemplate.swift │ │ │ │ ├── User/ │ │ │ │ │ ├── Authenticators/ │ │ │ │ │ │ ├── UserCredentialsAuthenticator.swift │ │ │ │ │ │ ├── UserSessionAuthenticator.swift │ │ │ │ │ │ └── UserTokenAuthenticator.swift │ │ │ │ │ ├── Controllers/ │ │ │ │ │ │ ├── UserApiController.swift │ │ │ │ │ │ └── UserFrontendController.swift │ │ │ │ │ ├── Database/ │ │ │ │ │ │ ├── Migrations/ │ │ │ │ │ │ │ └── UserMigrations.swift │ │ │ │ │ │ └── Models/ │ │ │ │ │ │ ├── UserAccountModel.swift │ │ │ │ │ │ └── UserTokenModel.swift │ │ │ │ │ ├── Forms/ │ │ │ │ │ │ └── UserLoginForm.swift │ │ │ │ │ ├── Templates/ │ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ │ └── UserLoginContext.swift │ │ │ │ │ │ └── Html/ │ │ │ │ │ │ └── UserLoginTemplate.swift │ │ │ │ │ ├── UserModule.swift │ │ │ │ │ └── UserRouter.swift │ │ │ │ └── Web/ │ │ │ │ ├── Controllers/ │ │ │ │ │ └── WebFrontendController.swift │ │ │ │ ├── Templates/ │ │ │ │ │ ├── Contexts/ │ │ │ │ │ │ ├── WebHomeContext.swift │ │ │ │ │ │ ├── WebIndexContext.swift │ │ │ │ │ │ └── WebLinkContext.swift │ │ │ │ │ └── Html/ │ │ │ │ │ ├── WebHomeTemplate.swift │ │ │ │ │ ├── WebIndexTemplate.swift │ │ │ │ │ └── WebLinkTemplate.swift │ │ │ │ ├── WebModule.swift │ │ │ │ └── WebRouter.swift │ │ │ ├── Template/ │ │ │ │ ├── Request+Template.swift │ │ │ │ ├── TemplateRenderer.swift │ │ │ │ └── TemplateRepresentable.swift │ │ │ ├── configure.swift │ │ │ └── routes.swift │ │ ├── AppApi/ │ │ │ ├── Framework/ │ │ │ │ ├── ApiModelInterface.swift │ │ │ │ └── ApiModuleInterface.swift │ │ │ └── Modules/ │ │ │ ├── Blog/ │ │ │ │ ├── Blog.swift │ │ │ │ ├── BlogCategory.swift │ │ │ │ └── BlogPost.swift │ │ │ └── User/ │ │ │ ├── User.swift │ │ │ ├── UserAccount.swift │ │ │ └── UserToken.swift │ │ └── Run/ │ │ └── main.swift │ ├── Tests/ │ │ ├── AppApiTests/ │ │ │ └── AppApiTests.swift │ │ └── AppTests/ │ │ ├── AppTests.swift │ │ ├── BlogCategoryApiTests.swift │ │ ├── BlogPostApiTests.swift │ │ └── Framework/ │ │ ├── AppTestCase.swift │ │ └── XCTApplicationTester.swift │ └── docker-compose.yml └── README.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .DS_Store .build .swiftpm xcuserdata MyProject.xcodeproj Packages DerivedData/ db.sqlite Package.resolved ================================================ FILE: Changelog.md ================================================ ## Changelog - Practical Server Side Swift Don't forget to check the sample code repository on [GitHub](https://github.com/tib/practical-server-side-swift). ### Third edition - v1.5.0 - Xcode 14.3 support - Swift 5.8 support - Vapor 4.70.0+ - SwiftHtml 1.7.0 - Reworked some parts of the book - Added some helpful notes & commands - Fixed some grammar mistakes & typos - Thanks to Ferenc (reviewer) & Michael (editor) ### Third edition - v1.4.0 - Swift async / await support - Xcode 13 support - Dropped Leaf / Tau - SwiftHtml DSL templates - Latest Vapor 4 version ### Second edition - v1.3.1 - Content updated to support Vapor 4.34 & Leaf Tau - Fixed trailing slashes in URL paths - Updated Vapor & Leaf Tau dependency - Fixed some grammar mistakes ### Second edition - v1.3.0 - Content updated to support Vapor 4.34 & Leaf Tau - Chapters from 10 until 16 are refactored - Simplified templates thanks to Leaf Tau - SPM tools v5.3 support - Updated Swift Package dependencies - Fixed some logical issues in code samples - It is now easier to follow the chapters - Pagination support for admin lists - Smaller fixes and improvements - New grammar mistakes & typos ### First edition - v1.2.0 - Content updated to support Vapor 4.14 - Fixed unowned reference issue - Fixed Blog migration issues using a dedicated seed - Sample .env.development file for Chapter 12 - Fixed some grammar mistakes ### First edition - v1.1.0 - Better document format with screenshots - Fixed some grammar mistakes - Introduced FILE name comment blocks ### First edition - v1.0.0 - First release ### Early Bird edition v1.0.0-beta-6 - Chapter 16 ### Early Bird edition v1.0.0-beta-5 - Chapter 15 ### Early Bird edition v1.0.0-beta-4 - Chapter 13 - Chapter 14 ### Early Bird edition v1.0.0-beta-3.1 - Added epub format ### Early Bird edition v1.0.0-beta-3 - Chapter 12 ### Early Bird edition v1.0.0-beta-2 - Chapter 11 ### Early Bird edition v1.0.0-beta-1 - Chapter 1-10 ================================================ FILE: Chapter 01/.gitkeep ================================================ ================================================ FILE: Chapter 02/SPM/.gitignore ================================================ .DS_Store /.build /Packages /*.xcodeproj xcuserdata/ ================================================ FILE: Chapter 02/SPM/Package.swift ================================================ // swift-tools-version:6.1 import PackageDescription let package = Package( name: "myProject", platforms: [ .macOS(.v12), ], products: [ .executable(name: "myProject", targets: ["myProject"]), ], dependencies: [ .package( url: "https://github.com/vapor/vapor", from: "4.115.0" ), ], targets: [ .executableTarget(name: "myProject", dependencies: [ .product(name: "Vapor", package: "vapor") ]), ] ) ================================================ FILE: Chapter 02/SPM/README.md ================================================ # myProject A description of this package. ================================================ FILE: Chapter 02/SPM/Sources/main.swift ================================================ import Vapor let env = try Environment.detect() let app = try await Application.make(env) do { app.get { req in "Hello Vapor!" } try await app.execute() } catch { try? await app.asyncShutdown() throw error } try await app.asyncShutdown() ================================================ FILE: Chapter 02/VaporToolbox/.dockerignore ================================================ .build/ .swiftpm/ ================================================ FILE: Chapter 02/VaporToolbox/.gitignore ================================================ Packages .build xcuserdata *.xcodeproj DerivedData/ .DS_Store db.sqlite .swiftpm .env .env.* !.env.example ================================================ FILE: Chapter 02/VaporToolbox/Dockerfile ================================================ # ================================ # Build image # ================================ FROM swift:6.0-noble 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 \ $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true) # Copy entire repo into container COPY . . # Build the application, 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 \ --product myProject \ --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)/myProject" ./ # 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:noble # 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 ["./myProject"] CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] ================================================ FILE: Chapter 02/VaporToolbox/Package.swift ================================================ // swift-tools-version:6.0 import PackageDescription let package = Package( name: "myProject", platforms: [ .macOS(.v13), ], dependencies: [ .package(url: "https://github.com/vapor/vapor", from: "4.115.0"), .package(url: "https://github.com/apple/swift-nio", from: "2.84.0"), ], targets: [ .executableTarget( name: "myProject", dependencies: [ .product(name: "Vapor", package: "vapor"), .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), ], swiftSettings: swiftSettings ), .testTarget( name: "myProjectTests", dependencies: [ .target(name: "myProject"), .product(name: "VaporTesting", package: "vapor"), ], swiftSettings: swiftSettings ) ] ) var swiftSettings: [SwiftSetting] { [ .enableUpcomingFeature("ExistentialAny"), ] } ================================================ FILE: Chapter 02/VaporToolbox/Public/.gitkeep ================================================ ================================================ FILE: Chapter 02/VaporToolbox/README.md ================================================ # myProject 💧 A project built with the Vapor web framework. ## Getting Started To build the project using the Swift Package Manager, run the following command in the terminal from the root of the project: ```bash swift build ``` To run the project and start the server, use the following command: ```bash swift run ``` To execute tests, use the following command: ```bash swift test ``` ### See more - [Vapor Website](https://vapor.codes) - [Vapor Documentation](https://docs.vapor.codes) - [Vapor GitHub](https://github.com/vapor) - [Vapor Community](https://github.com/vapor-community) ================================================ FILE: Chapter 02/VaporToolbox/Sources/myProject/Controllers/.gitkeep ================================================ ================================================ FILE: Chapter 02/VaporToolbox/Sources/myProject/configure.swift ================================================ 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)) // register routes try routes(app) } ================================================ FILE: Chapter 02/VaporToolbox/Sources/myProject/entrypoint.swift ================================================ import Vapor import Logging import NIOCore import NIOPosix @main enum Entrypoint { static func main() async throws { var env = try Environment.detect() try LoggingSystem.bootstrap(from: &env) let app = try await Application.make(env) do { try await configure(app) try await app.execute() } catch { app.logger.report(error: error) try? await app.asyncShutdown() throw error } try await app.asyncShutdown() } } ================================================ FILE: Chapter 02/VaporToolbox/Sources/myProject/routes.swift ================================================ import Vapor func routes(_ app: Application) throws { app.get { req async in "It works!" } app.get("hello") { req async -> String in "Hello, world!" } } ================================================ FILE: Chapter 02/VaporToolbox/Tests/myProjectTests/myProjectTests.swift ================================================ @testable import myProject import VaporTesting import Testing @Suite("App Tests") struct myProjectTests { @Test("Test Hello World Route") func helloWorld() async throws { try await withApp(configure: configure) { app in try await app.testing().test(.GET, "hello", afterResponse: { res async in #expect(res.status == .ok) #expect(res.body.string == "Hello, world!") }) } } } ================================================ FILE: Chapter 02/VaporToolbox/docker-compose.yml ================================================ # 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 # Stop all: docker compose down # x-shared_environment: &shared_environment LOG_LEVEL: ${LOG_LEVEL:-debug} services: app: image: myproject:latest build: context: . environment: <<: *shared_environment 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"] ================================================ FILE: Chapter 03/myProject/Dockerfile ================================================ # ================================ # Build image # ================================ FROM swift:6.0-noble 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 \ $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true) # Copy entire repo into container COPY . . # Build the application, 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 \ --product myProject \ --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)/myProject" ./ # 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:noble # 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 ["./myProject"] CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] ================================================ FILE: Chapter 03/myProject/Package.swift ================================================ // swift-tools-version:6.0 import PackageDescription let package = Package( name: "myProject", platforms: [ .macOS(.v13), ], dependencies: [ .package(url: "https://github.com/vapor/vapor", from: "4.115.0"), .package(url: "https://github.com/binarybirds/swift-html", from: "1.7.0"), ], targets: [ .executableTarget( name: "myProject", dependencies: [ .product(name: "Vapor", package: "vapor"), .product(name: "SwiftHtml", package: "swift-html"), .product(name: "SwiftSvg", package: "swift-html"), ], swiftSettings: swiftSettings ), .testTarget( name: "myProjectTests", dependencies: [ .target(name: "myProject"), .product(name: "VaporTesting", package: "vapor"), ], swiftSettings: swiftSettings ) ] ) var swiftSettings: [SwiftSetting] { [ .enableUpcomingFeature("ExistentialAny"), ] } ================================================ FILE: Chapter 03/myProject/Public/css/web.css ================================================ #blog h2 { margin: 0.5rem 0; } ================================================ FILE: Chapter 03/myProject/Public/js/web.js ================================================ function about() { alert("myPage\n\nversion 1.0.0"); } ================================================ FILE: Chapter 03/myProject/Sources/myProject/Middlewares/ExtendPathMiddleware.swift ================================================ import Vapor struct ExtendPathMiddleware: AsyncMiddleware { func respond( to req: Request, chainingTo next: any AsyncResponder ) async throws -> Response { if !req.url.path.hasSuffix("/") && !req.url.path.contains(".") { return req.redirect( to: req.url.path + "/", redirectType: .permanent ) } return try await next.respond( to: req ) } } ================================================ FILE: Chapter 03/myProject/Sources/myProject/Modules/Blog/BlogPost.swift ================================================ import Foundation struct BlogPost: Codable { let title: String let slug: String let image: String let excerpt: String let date: Date let category: String? let content: String } ================================================ FILE: Chapter 03/myProject/Sources/myProject/Modules/Blog/BlogRouter.swift ================================================ import Vapor struct BlogRouter: RouteCollection { let controller = BlogFrontendController() func boot( routes: any RoutesBuilder ) throws { routes.get("blog", use: controller.blogView) routes.get(.anything, use: controller.postView) } } ================================================ FILE: Chapter 03/myProject/Sources/myProject/Modules/Blog/Controllers/BlogFrontendController.swift ================================================ import Vapor struct BlogFrontendController { var posts: [BlogPost] = { stride(from: 1, to: 9, by: 1).map { index in BlogPost( title: "Sample post #\(index)", slug: "sample-post-\(index)", image: "/img/posts/\(String(format: "%02d", index + 1)).jpg", excerpt: "Lorem ipsum", date: Date().addingTimeInterval(-Double.random(in: 0...(86400 * 60))), category: Bool.random() ? "Sample category" : nil, content: "Lorem ipsum dolor sit amet." ) }.sorted() { $0.date > $1.date } }() func blogView( req: Request ) throws -> Response { let ctx = BlogPostsContext( icon: "🔥", title: "Blog", message: "Hot news and stories about everything.", posts: posts ) return req.templates.renderHtml( BlogPostsTemplate(ctx) ) } func postView( req: Request ) throws -> Response { let slug = req.url.path.trimmingCharacters( in: .init(charactersIn: "/") ) guard let post = posts.first(where: { $0.slug == slug }) else { return req.redirect(to: "/") } let ctx = BlogPostContext(post: post) return req.templates.renderHtml( BlogPostTemplate(ctx) ) } } ================================================ FILE: Chapter 03/myProject/Sources/myProject/Modules/Blog/Templates/Contexts/BlogPostContext.swift ================================================ struct BlogPostContext { let post: BlogPost } ================================================ FILE: Chapter 03/myProject/Sources/myProject/Modules/Blog/Templates/Contexts/BlogPostsContext.swift ================================================ struct BlogPostsContext { let icon: String let title: String let message: String let posts: [BlogPost] } ================================================ FILE: Chapter 03/myProject/Sources/myProject/Modules/Blog/Templates/Html/BlogPostTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostTemplate: TemplateRepresentable { var context: BlogPostContext init( _ context: BlogPostContext ) { self.context = context } var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .short return formatter }() @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.post.title) ) { Div { Section { P(dateFormatter.string(from: context.post.date)) H1(context.post.title) P(context.post.excerpt) } .class(["lead", "container"]) Img(src: context.post.image, alt: context.post.title) Article { Text(context.post.content) } .class("container") } .id("post") } .render(req) } } ================================================ FILE: Chapter 03/myProject/Sources/myProject/Modules/Blog/Templates/Html/BlogPostsTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostsTemplate: TemplateRepresentable { var context: BlogPostsContext init( _ context: BlogPostsContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") Div { for post in context.posts { Article { A { Img(src: post.image, alt: post.title) H2(post.title) P(post.excerpt) } .href("/\(post.slug)/") } } } .class("grid-221") } .id("blog") } .render(req) } } ================================================ FILE: Chapter 03/myProject/Sources/myProject/Modules/Web/Controllers/WebFrontendController.swift ================================================ import Vapor struct WebFrontendController { func homeView(req: Request) throws -> Response { let ctx = WebHomeContext( icon: "👋", title: "Home", message: "Hi there, welcome to my page.", paragraphs: [ "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.", "Nisi ut aliquip ex ea commodo consequat.", ], link: .init( label: "Read my blog →", url: "/blog/" ) ) return req.templates.renderHtml( WebHomeTemplate(ctx) ) } } ================================================ FILE: Chapter 03/myProject/Sources/myProject/Modules/Web/Templates/Contexts/WebHomeContext.swift ================================================ struct WebHomeContext { let icon: String let title: String let message: String let paragraphs: [String] let link: WebLinkContext } ================================================ FILE: Chapter 03/myProject/Sources/myProject/Modules/Web/Templates/Contexts/WebIndexContext.swift ================================================ public struct WebIndexContext { public let title: String public init( title: String ) { self.title = title } } ================================================ FILE: Chapter 03/myProject/Sources/myProject/Modules/Web/Templates/Contexts/WebLinkContext.swift ================================================ public struct WebLinkContext { public let label: String public let url: String public init( label: String, url: String ) { self.label = label self.url = url } } ================================================ FILE: Chapter 03/myProject/Sources/myProject/Modules/Web/Templates/Html/WebHomeTemplate.swift ================================================ import Vapor import SwiftHtml struct WebHomeTemplate: TemplateRepresentable { var context: WebHomeContext init( _ context: WebHomeContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") for paragraph in context.paragraphs { P(paragraph) } WebLinkTemplate(context.link).render(req) } .id("home") .class("container") } .render(req) } } ================================================ FILE: Chapter 03/myProject/Sources/myProject/Modules/Web/Templates/Html/WebIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg extension Svg { static func menuIcon() -> Svg { Svg { Line(x1: 3, y1: 12, x2: 21, y2: 12) Line(x1: 3, y1: 6, x2: 21, y2: 6) Line(x1: 3, y1: 18, x2: 21, y2: 18) } .width(24) .height(24) .viewBox(minX: 0, minY: 0, width: 24, height: 24) .fill("none") .stroke("currentColor") .strokeWidth(2) .strokeLinecap("round") .strokeLinejoin("round") } } public struct WebIndexTemplate: TemplateRepresentable { public var context: WebIndexContext var body: Tag public init( _ context: WebIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Link(rel: .shortcutIcon) .href("/img/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("/css/peacock.min.css") Link(rel: .stylesheet) .href("/css/web.css") Title(context.title) } Body { Header { Div { A { Img(src: "/img/logo.png", alt: "Logo") } .id("site-logo") .href("/") Nav { Input() .type(.checkbox) .id("primary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("primary-menu-button") Div { A("Home") .href("/") .class("selected", req.url.path == "/") A("Blog") .href("/blog/") .class("selected", req.url.path == "/blog/") A("About") .href("#") .onClick("javascript:about();") } .class("menu-items") } .id("primary-menu") } .id("navigation") } Main { body } Footer { Section { P { Text("This site is powered by ") A("Swift") .href("https://swift.org") .target(.blank) Text(" & ") A("Vapor") .href("https://vapor.codes") .target(.blank) Text(".") } P("myPage © 2020-2022") } } Script() .type(.javascript) .src("/js/web.js") } } .lang("en-US") } } ================================================ FILE: Chapter 03/myProject/Sources/myProject/Modules/Web/Templates/Html/WebLinkTemplate.swift ================================================ import Vapor import SwiftHtml struct WebLinkTemplate: TemplateRepresentable { var context: WebLinkContext init( _ context: WebLinkContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { A(context.label) .href(context.url) } } ================================================ FILE: Chapter 03/myProject/Sources/myProject/Modules/Web/WebRouter.swift ================================================ import Vapor struct WebRouter: RouteCollection { let frontendController = WebFrontendController() func boot( routes: any RoutesBuilder ) throws { routes.get(use: frontendController.homeView) } } ================================================ FILE: Chapter 03/myProject/Sources/myProject/Template/Request+Template.swift ================================================ import Vapor public extension Request { var templates: TemplateRenderer { .init(self) } } ================================================ FILE: Chapter 03/myProject/Sources/myProject/Template/TemplateRenderer.swift ================================================ import Vapor import SwiftHtml public struct TemplateRenderer { var req: Request init(_ req: Request) { self.req = req } public func renderHtml( _ template: any TemplateRepresentable, minify: Bool = false, indent: Int = 4 ) -> Response { let doc = Document(.html) { template.render(req) } let body = DocumentRenderer( minify: minify, indent: indent ) .render(doc) return Response( status: .ok, headers: [ "content-type": "text/html" ], body: .init(string: body) ) } } ================================================ FILE: Chapter 03/myProject/Sources/myProject/Template/TemplateRepresentable.swift ================================================ import Vapor import SwiftSgml public protocol TemplateRepresentable { @TagBuilder func render(_ req: Request) -> Tag } ================================================ FILE: Chapter 03/myProject/Sources/myProject/configure.swift ================================================ import Vapor public func configure( _ app: Application ) async throws { app.middleware.use( FileMiddleware( publicDirectory: app.directory.publicDirectory ) ) app.middleware.use(ExtendPathMiddleware()) let routers: [any RouteCollection] = [ WebRouter(), BlogRouter(), ] for router in routers { try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 03/myProject/Sources/myProject/entrypoint.swift ================================================ import Vapor import Logging @main enum Entrypoint { static func main() async throws { var env = try Environment.detect() try LoggingSystem.bootstrap(from: &env) let app = try await Application.make(env) do { try await configure(app) try await app.execute() } catch { app.logger.report(error: error) try? await app.asyncShutdown() throw error } try await app.asyncShutdown() } } ================================================ FILE: Chapter 03/myProject/Sources/myProject/routes.swift ================================================ import Vapor import SwiftHtml func routes(_ app: Application) throws { app.routes.get { req -> Response in req.templates.renderHtml( WebIndexTemplate( WebIndexContext( title: "Home" ) ) { P("Hi there, welcome to my page!") } ) } } ================================================ FILE: Chapter 03/myProject/Tests/myProjectTests/myProjectTests.swift ================================================ @testable import myProject import VaporTesting import Testing @Suite("App Tests") struct myProjectTests { @Test("Test Hello World Route") func helloWorld() async throws { try await withApp(configure: configure) { app in try await app.testing().test(.GET, "hello", afterResponse: { res async in #expect(res.status == .ok) #expect(res.body.string == "Hello, world!") }) } } } ================================================ FILE: Chapter 03/myProject/docker-compose.yml ================================================ # 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 # Stop all: docker compose down # x-shared_environment: &shared_environment LOG_LEVEL: ${LOG_LEVEL:-debug} services: app: image: myproject:latest build: context: . environment: <<: *shared_environment 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"] ================================================ FILE: Chapter 04/myProject/Dockerfile ================================================ # ================================ # Build image # ================================ FROM swift:6.0-noble 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 \ $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true) # Copy entire repo into container COPY . . # Build the application, 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 \ --product myProject \ --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)/myProject" ./ # 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:noble # 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 ["./myProject"] CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] ================================================ FILE: Chapter 04/myProject/Package.swift ================================================ // swift-tools-version:5.7 import PackageDescription let package = Package( name: "myProject", platforms: [ .macOS(.v12) ], dependencies: [ .package( url: "https://github.com/vapor/vapor", from: "4.70.0" ), .package( url: "https://github.com/vapor/fluent", from: "4.4.0" ), .package( url: "https://github.com/vapor/fluent-sqlite-driver", from: "4.1.0" ), .package( url: "https://github.com/binarybirds/swift-html", from: "1.7.0" ), ], targets: [ .target(name: "App", dependencies: [ .product(name: "Vapor", package: "vapor"), .product(name: "Fluent", package: "fluent"), .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), .product(name: "SwiftHtml", package: "swift-html"), .product(name: "SwiftSvg", package: "swift-html"), ]), .executableTarget(name: "Run", dependencies: ["App"]), .testTarget(name: "AppTests", dependencies: [ .target(name: "App"), .product(name: "XCTVapor", package: "vapor"), ]) ] ) ================================================ FILE: Chapter 04/myProject/Public/css/web.css ================================================ #blog h2 { margin: 0.5rem 0; } ================================================ FILE: Chapter 04/myProject/Public/js/web.js ================================================ function about() { alert("myPage\n\nversion 1.0.0"); } ================================================ FILE: Chapter 04/myProject/Sources/App/Framework/DatabaseModelInterface.swift ================================================ import Vapor import Fluent public protocol DatabaseModelInterface: Fluent.Model where Self.IDValue == UUID { associatedtype Module: ModuleInterface static var identifier: String { get } } public extension DatabaseModelInterface { static var schema: String { Module.identifier + "_" + identifier } static var identifier: String { String(describing: self) .dropFirst(Module.identifier.count) .dropLast(5) .lowercased() + "s" } } ================================================ FILE: Chapter 04/myProject/Sources/App/Framework/ModuleInterface.swift ================================================ import Vapor public protocol ModuleInterface { static var identifier: String { get } func boot(_ app: Application) throws } public extension ModuleInterface { func boot(_ app: Application) throws {} static var identifier: String { String(describing: self).dropLast(6).lowercased() } } ================================================ FILE: Chapter 04/myProject/Sources/App/Middlewares/ExtendPathMiddleware.swift ================================================ import Vapor struct ExtendPathMiddleware: AsyncMiddleware { func respond( to req: Request, chainingTo next: AsyncResponder ) async throws -> Response { if !req.url.path.hasSuffix("/") && !req.url.path.contains(".") { return req.redirect( to: req.url.path + "/", redirectType: .permanent ) } return try await next.respond( to: req ) } } ================================================ FILE: Chapter 04/myProject/Sources/App/Modules/Blog/BlogModule.swift ================================================ import Vapor struct BlogModule: ModuleInterface { let router = BlogRouter() func boot(_ app: Application) throws { app.migrations.add(BlogMigrations.v1()) app.migrations.add(BlogMigrations.seed()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 04/myProject/Sources/App/Modules/Blog/BlogRouter.swift ================================================ import Vapor struct BlogRouter: RouteCollection { let controller = BlogFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get("blog", use: controller.blogView) routes.get(.anything, use: controller.postView) } } ================================================ FILE: Chapter 04/myProject/Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift ================================================ import Vapor import Fluent struct BlogFrontendController { func blogView(req: Request) async throws -> Response { let postModels = try await BlogPostModel .query(on: req.db) .sort(\.$date, .descending) .all() let posts = try postModels.map { Blog.Post.List( id: try $0.requireID(), title: $0.title, slug: $0.slug, image: $0.imageKey, excerpt: $0.excerpt, date: $0.date ) } let ctx = BlogPostsContext( icon: "🔥", title: "Blog", message: "Hot news and stories about everything.", posts: posts) return req.templates.renderHtml( BlogPostsTemplate(ctx) ) } func postView(req: Request) async throws -> Response { let slug = req.url.path.trimmingCharacters( in: .init(charactersIn: "/") ) guard let post = try await BlogPostModel .query(on: req.db) .filter(\.$slug == slug) .with(\.$category) .first() else { return req.redirect(to: "/") } let ctx = BlogPostContext( post: Blog.Post.Detail( id: post.id!, title: post.title, slug: post.slug, image: post.imageKey, excerpt: post.excerpt, date: post.date, category: .init( id: post.category.id!, title: post.category.title ), content: post.content ) ) return req.templates.renderHtml( BlogPostTemplate(ctx) ) } } ================================================ FILE: Chapter 04/myProject/Sources/App/Modules/Blog/Database/Migrations/BlogMigrations.swift ================================================ import Foundation import Fluent enum BlogMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema) .id() .field(BlogCategoryModel.FieldKeys.v1.title, .string, .required) .create() try await db.schema(BlogPostModel.schema) .id() .field(BlogPostModel.FieldKeys.v1.title, .string, .required) .field(BlogPostModel.FieldKeys.v1.slug, .string, .required) .field(BlogPostModel.FieldKeys.v1.imageKey, .string, .required) .field(BlogPostModel.FieldKeys.v1.excerpt, .data, .required) .field(BlogPostModel.FieldKeys.v1.date, .datetime, .required) .field(BlogPostModel.FieldKeys.v1.content, .data, .required) .field(BlogPostModel.FieldKeys.v1.categoryId, .uuid) .foreignKey( BlogPostModel.FieldKeys.v1.categoryId, references: BlogCategoryModel.schema, .id, onDelete: DatabaseSchema.ForeignKeyAction.setNull, onUpdate: .cascade ) .unique(on: BlogPostModel.FieldKeys.v1.slug) .create() } func revert(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema).delete() try await db.schema(BlogPostModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let categories = (1...4).map { index in BlogCategoryModel(title: "Sample category #\(index)") } try await categories.create(on: db) try await (1...9).map { index in BlogPostModel( id: nil, title: "Sample post #\(index)", slug: "sample-post-\(index)", imageKey: "/img/posts/\(String(format: "%02d", index + 1)).jpg", excerpt: "Lorem ipsum", date: Date().addingTimeInterval(-Double.random(in: 0...(86400 * 60))), content: "Lorem ipsum dolor sit amet.", categoryId: categories[Int.random(in: 0.. Tag { WebIndexTemplate( .init(title: context.post.title) ) { Div { Section { P(dateFormatter.string(from: context.post.date)) H1(context.post.title) P(context.post.excerpt) } .class(["lead", "container"]) Img(src: context.post.image, alt: context.post.title) Article { Text(context.post.content) } .class("container") } .id("post") } .render(req) } } ================================================ FILE: Chapter 04/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostsTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostsTemplate: TemplateRepresentable { var context: BlogPostsContext init( _ context: BlogPostsContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") Div { for post in context.posts { Article { A { Img(src: post.image, alt: post.title) H2(post.title) P(post.excerpt) } .href("/\(post.slug)/") } } } .class("grid-221") } .id("blog") } .render(req) } } ================================================ FILE: Chapter 04/myProject/Sources/App/Modules/Web/Controllers/WebFrontendController.swift ================================================ import Vapor struct WebFrontendController { func homeView(req: Request) throws -> Response { let ctx = WebHomeContext( icon: "👋", title: "Home", message: "Hi there, welcome to my page.", paragraphs: [ "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.", "Nisi ut aliquip ex ea commodo consequat.", ], link: .init( label: "Read my blog →", url: "/blog/" ) ) return req.templates.renderHtml( WebHomeTemplate(ctx) ) } } ================================================ FILE: Chapter 04/myProject/Sources/App/Modules/Web/Templates/Contexts/WebHomeContext.swift ================================================ struct WebHomeContext { let icon: String let title: String let message: String let paragraphs: [String] let link: WebLinkContext } ================================================ FILE: Chapter 04/myProject/Sources/App/Modules/Web/Templates/Contexts/WebIndexContext.swift ================================================ public struct WebIndexContext { public let title: String public init( title: String ) { self.title = title } } ================================================ FILE: Chapter 04/myProject/Sources/App/Modules/Web/Templates/Contexts/WebLinkContext.swift ================================================ public struct WebLinkContext { public let label: String public let url: String public init( label: String, url: String ) { self.label = label self.url = url } } ================================================ FILE: Chapter 04/myProject/Sources/App/Modules/Web/Templates/Html/WebHomeTemplate.swift ================================================ import Vapor import SwiftHtml struct WebHomeTemplate: TemplateRepresentable { var context: WebHomeContext init( _ context: WebHomeContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") for paragraph in context.paragraphs { P(paragraph) } WebLinkTemplate(context.link).render(req) } .id("home") .class("container") } .render(req) } } ================================================ FILE: Chapter 04/myProject/Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg extension Svg { static func menuIcon() -> Svg { Svg { Line(x1: 3, y1: 12, x2: 21, y2: 12) Line(x1: 3, y1: 6, x2: 21, y2: 6) Line(x1: 3, y1: 18, x2: 21, y2: 18) } .width(24) .height(24) .viewBox(minX: 0, minY: 0, width: 24, height: 24) .fill("none") .stroke("currentColor") .strokeWidth(2) .strokeLinecap("round") .strokeLinejoin("round") } } public struct WebIndexTemplate: TemplateRepresentable { public var context: WebIndexContext var body: Tag public init( _ context: WebIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Link(rel: .shortcutIcon) .href("/img/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css") Link(rel: .stylesheet) .href("/css/web.css") Title(context.title) } Body { Header { Div { A { Img(src: "/img/logo.png", alt: "Logo") } .id("site-logo") .href("/") Nav { Input() .type(.checkbox) .id("primary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("primary-menu-button") Div { A("Home") .href("/") .class("selected", req.url.path == "/") A("Blog") .href("/blog/") .class("selected", req.url.path == "/blog/") A("About") .href("#") .onClick("javascript:about();") } .class("menu-items") } .id("primary-menu") } .id("navigation") } Main { body } Footer { Section { P { Text("This site is powered by ") A("Swift") .href("https://swift.org") .target(.blank) Text(" & ") A("Vapor") .href("https://vapor.codes") .target(.blank) Text(".") } P("myPage © 2020-2022") } } Script() .type(.javascript) .src("/js/web.js") } } .lang("en-US") } } ================================================ FILE: Chapter 04/myProject/Sources/App/Modules/Web/Templates/Html/WebLinkTemplate.swift ================================================ import Vapor import SwiftHtml struct WebLinkTemplate: TemplateRepresentable { var context: WebLinkContext init( _ context: WebLinkContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { A(context.label) .href(context.url) } } ================================================ FILE: Chapter 04/myProject/Sources/App/Modules/Web/WebModule.swift ================================================ import Vapor struct WebModule: ModuleInterface { let router = WebRouter() func boot(_ app: Application) throws { try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 04/myProject/Sources/App/Modules/Web/WebRouter.swift ================================================ import Vapor struct WebRouter: RouteCollection { let frontendController = WebFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get(use: frontendController.homeView) } } ================================================ FILE: Chapter 04/myProject/Sources/App/Template/Request+Template.swift ================================================ import Vapor public extension Request { var templates: TemplateRenderer { .init(self) } } ================================================ FILE: Chapter 04/myProject/Sources/App/Template/TemplateRenderer.swift ================================================ import Vapor import SwiftHtml public struct TemplateRenderer { var req: Request init(_ req: Request) { self.req = req } public func renderHtml( _ template: TemplateRepresentable, minify: Bool = false, indent: Int = 4 ) -> Response { let doc = Document(.html) { template.render(req) } let body = DocumentRenderer( minify: minify, indent: indent ) .render(doc) return Response( status: .ok, headers: [ "content-type": "text/html" ], body: .init(string: body) ) } } ================================================ FILE: Chapter 04/myProject/Sources/App/Template/TemplateRepresentable.swift ================================================ import Vapor import SwiftSgml public protocol TemplateRepresentable { @TagBuilder func render(_ req: Request) -> Tag } ================================================ FILE: Chapter 04/myProject/Sources/App/configure.swift ================================================ import Vapor import Fluent import FluentSQLiteDriver public func configure( _ app: Application ) throws { let dbPath = app.directory.resourcesDirectory + "db.sqlite" app.databases.use(.sqlite(.file(dbPath)), as: .sqlite) app.middleware.use( FileMiddleware( publicDirectory: app.directory.publicDirectory ) ) app.middleware.use(ExtendPathMiddleware()) let modules: [ModuleInterface] = [ WebModule(), BlogModule(), ] for module in modules { try module.boot(app) } try app.autoMigrate().wait() } ================================================ FILE: Chapter 04/myProject/Sources/App/routes.swift ================================================ import Vapor import SwiftHtml func routes(_ app: Application) throws { app.routes.get { req -> Response in req.templates.renderHtml( WebIndexTemplate( WebIndexContext( title: "Home" ) ) { P("Hi there, welcome to my page!") } ) } } ================================================ FILE: Chapter 04/myProject/Sources/Run/main.swift ================================================ import App import Vapor var env = try Environment.detect() try LoggingSystem.bootstrap(from: &env) let app = Application(env) defer { app.shutdown() } try configure(app) try app.run() ================================================ FILE: Chapter 04/myProject/Tests/AppTests/AppTests.swift ================================================ @testable import App import XCTVapor final class AppTests: XCTestCase { func testHelloWorld() throws { let app = Application(.testing) defer { app.shutdown() } try configure(app) try app.test(.GET, "hello", afterResponse: { res in XCTAssertEqual(res.status, .ok) XCTAssertEqual(res.body.string, "Hello, world!") }) } } ================================================ FILE: Chapter 04/myProject/docker-compose.yml ================================================ # 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 # Stop all: docker compose down # x-shared_environment: &shared_environment LOG_LEVEL: ${LOG_LEVEL:-debug} services: app: image: myproject:latest build: context: . environment: <<: *shared_environment 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"] ================================================ FILE: Chapter 05/myProject/Dockerfile ================================================ # ================================ # Build image # ================================ FROM swift:6.0-noble 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 \ $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true) # Copy entire repo into container COPY . . # Build the application, 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 \ --product myProject \ --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)/myProject" ./ # 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:noble # 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 ["./myProject"] CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] ================================================ FILE: Chapter 05/myProject/Package.swift ================================================ // swift-tools-version:5.7 import PackageDescription let package = Package( name: "myProject", platforms: [ .macOS(.v12) ], dependencies: [ .package( url: "https://github.com/vapor/vapor", from: "4.70.0" ), .package( url: "https://github.com/vapor/fluent", from: "4.4.0" ), .package( url: "https://github.com/vapor/fluent-sqlite-driver", from: "4.1.0" ), .package( url: "https://github.com/binarybirds/swift-html", from: "1.7.0" ), ], targets: [ .target(name: "App", dependencies: [ .product(name: "Vapor", package: "vapor"), .product(name: "Fluent", package: "fluent"), .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), .product(name: "SwiftHtml", package: "swift-html"), .product(name: "SwiftSvg", package: "swift-html"), ]), .executableTarget(name: "Run", dependencies: ["App"]), .testTarget(name: "AppTests", dependencies: [ .target(name: "App"), .product(name: "XCTVapor", package: "vapor"), ]) ] ) ================================================ FILE: Chapter 05/myProject/Public/css/web.css ================================================ #blog h2 { margin: 0.5rem 0; } ================================================ FILE: Chapter 05/myProject/Public/js/web.js ================================================ function about() { alert("myPage\n\nversion 1.0.0"); } ================================================ FILE: Chapter 05/myProject/Sources/App/Framework/AuthenticatedUser.swift ================================================ import Vapor public struct AuthenticatedUser { public let id: UUID public let email: String public init( id: UUID, email: String ) { self.id = id self.email = email } } extension AuthenticatedUser: SessionAuthenticatable { public var sessionID: UUID { id } } ================================================ FILE: Chapter 05/myProject/Sources/App/Framework/DatabaseModelInterface.swift ================================================ import Vapor import Fluent public protocol DatabaseModelInterface: Fluent.Model where Self.IDValue == UUID { associatedtype Module: ModuleInterface static var identifier: String { get } } public extension DatabaseModelInterface { static var schema: String { Module.identifier + "_" + identifier } static var identifier: String { String(describing: self) .dropFirst(Module.identifier.count) .dropLast(5) .lowercased() + "s" } } ================================================ FILE: Chapter 05/myProject/Sources/App/Framework/ModuleInterface.swift ================================================ import Vapor public protocol ModuleInterface { static var identifier: String { get } func boot(_ app: Application) throws } public extension ModuleInterface { func boot(_ app: Application) throws {} static var identifier: String { String(describing: self).dropLast(6).lowercased() } } ================================================ FILE: Chapter 05/myProject/Sources/App/Middlewares/ExtendPathMiddleware.swift ================================================ import Vapor struct ExtendPathMiddleware: AsyncMiddleware { func respond( to req: Request, chainingTo next: AsyncResponder ) async throws -> Response { if !req.url.path.hasSuffix("/") && !req.url.path.contains(".") { return req.redirect( to: req.url.path + "/", redirectType: .permanent ) } return try await next.respond( to: req ) } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/Blog/BlogModule.swift ================================================ import Vapor struct BlogModule: ModuleInterface { let router = BlogRouter() func boot(_ app: Application) throws { app.migrations.add(BlogMigrations.v1()) app.migrations.add(BlogMigrations.seed()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/Blog/BlogRouter.swift ================================================ import Vapor struct BlogRouter: RouteCollection { let controller = BlogFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get("blog", use: controller.blogView) routes.get(.anything, use: controller.postView) } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift ================================================ import Vapor import Fluent struct BlogFrontendController { func blogView(req: Request) async throws -> Response { let postModels = try await BlogPostModel .query(on: req.db) .sort(\.$date, .descending) .all() let posts = try postModels.map { Blog.Post.List( id: try $0.requireID(), title: $0.title, slug: $0.slug, image: $0.imageKey, excerpt: $0.excerpt, date: $0.date ) } let ctx = BlogPostsContext( icon: "🔥", title: "Blog", message: "Hot news and stories about everything.", posts: posts) return req.templates.renderHtml( BlogPostsTemplate(ctx) ) } func postView(req: Request) async throws -> Response { let slug = req.url.path.trimmingCharacters( in: .init(charactersIn: "/") ) guard let post = try await BlogPostModel .query(on: req.db) .filter(\.$slug == slug) .with(\.$category) .first() else { return req.redirect(to: "/") } let ctx = BlogPostContext( post: Blog.Post.Detail( id: post.id!, title: post.title, slug: post.slug, image: post.imageKey, excerpt: post.excerpt, date: post.date, category: .init( id: post.category.id!, title: post.category.title ), content: post.content ) ) return req.templates.renderHtml( BlogPostTemplate(ctx) ) } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/Blog/Database/Migrations/BlogMigrations.swift ================================================ import Foundation import Fluent enum BlogMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema) .id() .field(BlogCategoryModel.FieldKeys.v1.title, .string, .required) .create() try await db.schema(BlogPostModel.schema) .id() .field(BlogPostModel.FieldKeys.v1.title, .string, .required) .field(BlogPostModel.FieldKeys.v1.slug, .string, .required) .field(BlogPostModel.FieldKeys.v1.imageKey, .string, .required) .field(BlogPostModel.FieldKeys.v1.excerpt, .data, .required) .field(BlogPostModel.FieldKeys.v1.date, .datetime, .required) .field(BlogPostModel.FieldKeys.v1.content, .data, .required) .field(BlogPostModel.FieldKeys.v1.categoryId, .uuid) .foreignKey( BlogPostModel.FieldKeys.v1.categoryId, references: BlogCategoryModel.schema, .id, onDelete: DatabaseSchema.ForeignKeyAction.setNull, onUpdate: .cascade ) .unique(on: BlogPostModel.FieldKeys.v1.slug) .create() } func revert(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema).delete() try await db.schema(BlogPostModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let categories = (1...4).map { index in BlogCategoryModel(title: "Sample category #\(index)") } try await categories.create(on: db) try await (1...9).map { index in BlogPostModel( id: nil, title: "Sample post #\(index)", slug: "sample-post-\(index)", imageKey: "/img/posts/\(String(format: "%02d", index + 1)).jpg", excerpt: "Lorem ipsum", date: Date().addingTimeInterval(-Double.random(in: 0...(86400 * 60))), content: "Lorem ipsum dolor sit amet.", categoryId: categories[Int.random(in: 0.. Tag { WebIndexTemplate( .init(title: context.post.title) ) { Div { Section { P(dateFormatter.string(from: context.post.date)) H1(context.post.title) P(context.post.excerpt) } .class(["lead", "container"]) Img(src: context.post.image, alt: context.post.title) Article { Text(context.post.content) } .class("container") } .id("post") } .render(req) } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostsTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostsTemplate: TemplateRepresentable { var context: BlogPostsContext init( _ context: BlogPostsContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") Div { for post in context.posts { Article { A { Img(src: post.image, alt: post.title) H2(post.title) P(post.excerpt) } .href("/\(post.slug)/") } } } .class("grid-221") } .id("blog") } .render(req) } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/User/Authenticators/UserCredentialsAuthenticator.swift ================================================ import Vapor import Fluent struct UserCredentialsAuthenticator: AsyncCredentialsAuthenticator { struct Credentials: Content { let email: String let password: String } func authenticate( credentials: Credentials, for req: Request ) async throws { guard let user = try await UserAccountModel .query(on: req.db) .filter(\.$email == credentials.email) .first() else { return } do { guard try Bcrypt.verify( credentials.password, created: user.password ) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } catch { // do nothing... } } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/User/Authenticators/UserSessionAuthenticator.swift ================================================ import Vapor import Fluent struct UserSessionAuthenticator: AsyncSessionAuthenticator { typealias User = AuthenticatedUser func authenticate( sessionID: User.SessionID, for req: Request ) async throws { guard let user = try await UserAccountModel .find(sessionID, on: req.db) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/User/Controllers/UserFrontendController.swift ================================================ import Vapor struct UserFrontendController { private struct Input: Decodable { let email: String? let password: String? } private func renderSignInView( _ req: Request, _ input: Input? = nil, _ error: String? = nil ) -> Response { let template = UserLoginTemplate( .init( icon: "⬇️", title: "Sign in", message: "Please log in with your existing account", email: input?.email, password: input?.password, error: error ) ) return req.templates.renderHtml(template) } func signInView(_ req: Request) async throws -> Response { renderSignInView(req) } func signInAction(_ req: Request) async throws -> Response { /// if the user is authenticated, we can store the user data inside the session too if let user = req.auth.get(AuthenticatedUser.self) { req.session.authenticate(user) return req.redirect(to: "/") } /// if the user credentials were wrong we render the form again with an error message let input = try req.content.decode(Input.self) return renderSignInView( req, input, "Invalid email or password." ) } func signOut(req: Request) throws -> Response { req.auth.logout(AuthenticatedUser.self) req.session.unauthenticate(AuthenticatedUser.self) // req.session.destroy() return req.redirect(to: "/") } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/User/Database/Migrations/UserMigrations.swift ================================================ import Vapor import Fluent enum UserMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(UserAccountModel.schema) .id() .field(UserAccountModel.FieldKeys.v1.email, .string, .required) .field(UserAccountModel.FieldKeys.v1.password, .string, .required) .unique(on: UserAccountModel.FieldKeys.v1.email) .create() } func revert(on db: Database) async throws { try await db.schema(UserAccountModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let email = "root@localhost.com" let password = "ChangeMe1" let user = UserAccountModel( email: email, password: try Bcrypt.hash(password) ) try await user.create(on: db) } func revert(on db: Database) async throws { try await UserAccountModel.query(on: db).delete() } } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/User/Database/Models/UserAccountModel.swift ================================================ import Vapor import Fluent final class UserAccountModel: DatabaseModelInterface { typealias Module = UserModule struct FieldKeys { struct v1 { static var email: FieldKey { "email" } static var password: FieldKey { "password" } } } @ID() var id: UUID? @Field(key: FieldKeys.v1.email) var email: String @Field(key: FieldKeys.v1.password) var password: String init() { } init( id: UUID? = nil, email: String, password: String ) { self.id = id self.email = email self.password = password } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/User/Templates/Contexts/UserLoginContext.swift ================================================ struct UserLoginContext { let icon: String let title: String let message: String let email: String? let password: String? let error: String? init( icon: String, title: String, message: String, email: String? = nil, password: String? = nil, error: String? = nil ) { self.icon = icon self.title = title self.message = message self.email = email self.password = password self.error = error } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/User/Templates/Html/UserLoginTemplate.swift ================================================ import Vapor import SwiftHtml struct UserLoginTemplate: TemplateRepresentable { var context: UserLoginContext init( _ context: UserLoginContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") Form { if let error = context.error { Section { Span(error) .class("error") } } Section { Label("Email:") .for("email") Input() .key("email") .type(.email) .value(context.email) .class("field") } Section { Label("Password:") .for("password") Input() .key("password") .type(.password) .value(context.password) .class("field") } Section { Input() .type(.submit) .value("Sign in") .class("submit") } } .action("/sign-in/") .method(.post) } .id("user-login") .class("container") } .render(req) } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/User/UserModule.swift ================================================ import Vapor struct UserModule: ModuleInterface { let router = UserRouter() func boot(_ app: Application) throws { app.migrations.add(UserMigrations.v1()) app.migrations.add(UserMigrations.seed()) app.middleware.use(UserSessionAuthenticator()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/User/UserRouter.swift ================================================ import Vapor struct UserRouter: RouteCollection { let frontendController = UserFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get("sign-in", use: frontendController.signInView) routes .grouped( UserCredentialsAuthenticator() ) .post("sign-in", use: frontendController.signInAction) routes.get("sign-out", use: frontendController.signOut) } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/Web/Controllers/WebFrontendController.swift ================================================ import Vapor struct WebFrontendController { func homeView(req: Request) throws -> Response { let ctx = WebHomeContext( icon: "👋", title: "Home", message: "Hi there, welcome to my page.", paragraphs: [ "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.", "Nisi ut aliquip ex ea commodo consequat.", ], link: .init( label: "Read my blog →", url: "/blog/" ) ) return req.templates.renderHtml( WebHomeTemplate(ctx) ) } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/Web/Templates/Contexts/WebHomeContext.swift ================================================ struct WebHomeContext { let icon: String let title: String let message: String let paragraphs: [String] let link: WebLinkContext } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/Web/Templates/Contexts/WebIndexContext.swift ================================================ public struct WebIndexContext { public let title: String public init( title: String ) { self.title = title } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/Web/Templates/Contexts/WebLinkContext.swift ================================================ public struct WebLinkContext { public let label: String public let url: String public init( label: String, url: String ) { self.label = label self.url = url } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/Web/Templates/Html/WebHomeTemplate.swift ================================================ import Vapor import SwiftHtml struct WebHomeTemplate: TemplateRepresentable { var context: WebHomeContext init( _ context: WebHomeContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") for paragraph in context.paragraphs { P(paragraph) } WebLinkTemplate(context.link).render(req) } .id("home") .class("container") } .render(req) } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg extension Svg { static func menuIcon() -> Svg { Svg { Line(x1: 3, y1: 12, x2: 21, y2: 12) Line(x1: 3, y1: 6, x2: 21, y2: 6) Line(x1: 3, y1: 18, x2: 21, y2: 18) } .width(24) .height(24) .viewBox(minX: 0, minY: 0, width: 24, height: 24) .fill("none") .stroke("currentColor") .strokeWidth(2) .strokeLinecap("round") .strokeLinejoin("round") } } public struct WebIndexTemplate: TemplateRepresentable { public var context: WebIndexContext var body: Tag public init( _ context: WebIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Link(rel: .shortcutIcon) .href("/img/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css") Link(rel: .stylesheet) .href("/css/web.css") Title(context.title) } Body { Header { Div { A { Img(src: "/img/logo.png", alt: "Logo") } .id("site-logo") .href("/") Nav { Input() .type(.checkbox) .id("primary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("primary-menu-button") Div { A("Home") .href("/") .class("selected", req.url.path == "/") A("Blog") .href("/blog/") .class("selected", req.url.path == "/blog/") A("About") .href("#") .onClick("javascript:about();") if req.auth.has(AuthenticatedUser.self) { A("Sign out") .href("/sign-out/") } else { A("Sign in") .href("/sign-in/") } } .class("menu-items") } .id("primary-menu") } .id("navigation") } Main { body } Footer { Section { P { Text("This site is powered by ") A("Swift") .href("https://swift.org") .target(.blank) Text(" & ") A("Vapor") .href("https://vapor.codes") .target(.blank) Text(".") } P("myPage © 2020-2022") } } Script() .type(.javascript) .src("/js/web.js") } } .lang("en-US") } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/Web/Templates/Html/WebLinkTemplate.swift ================================================ import Vapor import SwiftHtml struct WebLinkTemplate: TemplateRepresentable { var context: WebLinkContext init( _ context: WebLinkContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { A(context.label) .href(context.url) } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/Web/WebModule.swift ================================================ import Vapor struct WebModule: ModuleInterface { let router = WebRouter() func boot(_ app: Application) throws { try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 05/myProject/Sources/App/Modules/Web/WebRouter.swift ================================================ import Vapor struct WebRouter: RouteCollection { let frontendController = WebFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get(use: frontendController.homeView) } } ================================================ FILE: Chapter 05/myProject/Sources/App/Template/Request+Template.swift ================================================ import Vapor public extension Request { var templates: TemplateRenderer { .init(self) } } ================================================ FILE: Chapter 05/myProject/Sources/App/Template/TemplateRenderer.swift ================================================ import Vapor import SwiftHtml public struct TemplateRenderer { var req: Request init(_ req: Request) { self.req = req } public func renderHtml( _ template: TemplateRepresentable, minify: Bool = false, indent: Int = 4 ) -> Response { let doc = Document(.html) { template.render(req) } let body = DocumentRenderer( minify: minify, indent: indent ) .render(doc) return Response( status: .ok, headers: [ "content-type": "text/html" ], body: .init(string: body) ) } } ================================================ FILE: Chapter 05/myProject/Sources/App/Template/TemplateRepresentable.swift ================================================ import Vapor import SwiftSgml public protocol TemplateRepresentable { @TagBuilder func render(_ req: Request) -> Tag } ================================================ FILE: Chapter 05/myProject/Sources/App/configure.swift ================================================ import Vapor import Fluent import FluentSQLiteDriver public func configure( _ app: Application ) throws { let dbPath = app.directory.resourcesDirectory + "db.sqlite" app.databases.use(.sqlite(.file(dbPath)), as: .sqlite) app.middleware.use( FileMiddleware( publicDirectory: app.directory.publicDirectory ) ) app.middleware.use(ExtendPathMiddleware()) app.sessions.use(.fluent) app.migrations.add(SessionRecord.migration) app.middleware.use(app.sessions.middleware) let modules: [ModuleInterface] = [ WebModule(), UserModule(), BlogModule(), ] for module in modules { try module.boot(app) } try app.autoMigrate().wait() } ================================================ FILE: Chapter 05/myProject/Sources/App/routes.swift ================================================ import Vapor import SwiftHtml func routes(_ app: Application) throws { app.routes.get { req -> Response in req.templates.renderHtml( WebIndexTemplate( WebIndexContext( title: "Home" ) ) { P("Hi there, welcome to my page!") } ) } } ================================================ FILE: Chapter 05/myProject/Sources/Run/main.swift ================================================ import App import Vapor var env = try Environment.detect() try LoggingSystem.bootstrap(from: &env) let app = Application(env) defer { app.shutdown() } try configure(app) try app.run() ================================================ FILE: Chapter 05/myProject/Tests/AppTests/AppTests.swift ================================================ @testable import App import XCTVapor final class AppTests: XCTestCase { func testHelloWorld() throws { let app = Application(.testing) defer { app.shutdown() } try configure(app) try app.test(.GET, "hello", afterResponse: { res in XCTAssertEqual(res.status, .ok) XCTAssertEqual(res.body.string, "Hello, world!") }) } } ================================================ FILE: Chapter 05/myProject/docker-compose.yml ================================================ # 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 # Stop all: docker compose down # x-shared_environment: &shared_environment LOG_LEVEL: ${LOG_LEVEL:-debug} services: app: image: myproject:latest build: context: . environment: <<: *shared_environment 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"] ================================================ FILE: Chapter 06/myProject/Dockerfile ================================================ # ================================ # Build image # ================================ FROM swift:6.0-noble 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 \ $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true) # Copy entire repo into container COPY . . # Build the application, 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 \ --product myProject \ --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)/myProject" ./ # 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:noble # 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 ["./myProject"] CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] ================================================ FILE: Chapter 06/myProject/Package.swift ================================================ // swift-tools-version:5.7 import PackageDescription let package = Package( name: "myProject", platforms: [ .macOS(.v12) ], dependencies: [ .package( url: "https://github.com/vapor/vapor", from: "4.70.0" ), .package( url: "https://github.com/vapor/fluent", from: "4.4.0" ), .package( url: "https://github.com/vapor/fluent-sqlite-driver", from: "4.1.0" ), .package( url: "https://github.com/binarybirds/swift-html", from: "1.7.0" ), ], targets: [ .target(name: "App", dependencies: [ .product(name: "Vapor", package: "vapor"), .product(name: "Fluent", package: "fluent"), .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), .product(name: "SwiftHtml", package: "swift-html"), .product(name: "SwiftSvg", package: "swift-html"), ]), .executableTarget(name: "Run", dependencies: ["App"]), .testTarget(name: "AppTests", dependencies: [ .target(name: "App"), .product(name: "XCTVapor", package: "vapor"), ]) ] ) ================================================ FILE: Chapter 06/myProject/Public/css/web.css ================================================ #blog h2 { margin: 0.5rem 0; } ================================================ FILE: Chapter 06/myProject/Public/js/web.js ================================================ function about() { alert("myPage\n\nversion 1.0.0"); } ================================================ FILE: Chapter 06/myProject/Sources/App/Framework/AuthenticatedUser.swift ================================================ import Vapor public struct AuthenticatedUser { public let id: UUID public let email: String public init( id: UUID, email: String ) { self.id = id self.email = email } } extension AuthenticatedUser: SessionAuthenticatable { public var sessionID: UUID { id } } ================================================ FILE: Chapter 06/myProject/Sources/App/Framework/DatabaseModelInterface.swift ================================================ import Vapor import Fluent public protocol DatabaseModelInterface: Fluent.Model where Self.IDValue == UUID { associatedtype Module: ModuleInterface static var identifier: String { get } } public extension DatabaseModelInterface { static var schema: String { Module.identifier + "_" + identifier } static var identifier: String { String(describing: self) .dropFirst(Module.identifier.count) .dropLast(5) .lowercased() + "s" } } ================================================ FILE: Chapter 06/myProject/Sources/App/Framework/Form/AbstractForm.swift ================================================ import Vapor open class AbstractForm: FormComponent { open var action: FormAction open var fields: [FormComponent] open var error: String? open var submit: String? public init( action: FormAction = .init(), fields: [FormComponent] = [], error: String? = nil, submit: String? = nil ) { self.action = action self.fields = fields self.error = error self.submit = submit self.action.enctype = .multipart } open func load(req: Request) async throws { for field in fields { try await field.load(req: req) } } open func process(req: Request) async throws { for field in fields { try await field.process(req: req) } } open func validate(req: Request) async throws -> Bool { var result: [Bool] = [] for field in fields { result.append(try await field.validate(req: req)) } return result.filter { $0 == false }.isEmpty } open func write(req: Request) async throws { for field in fields { try await field.write(req: req) } } open func save(req: Request) async throws { for field in fields { try await field.save(req: req) } } open func read(req: Request) async throws { for field in fields { try await field.read(req: req) } } open func render(req: Request) -> TemplateRepresentable { FormTemplate( .init( action: action, fields: fields.map { $0.render(req: req)}, error: error, submit: submit ) ) } } ================================================ FILE: Chapter 06/myProject/Sources/App/Framework/Form/AbstractFormField.swift ================================================ import Vapor open class AbstractFormField< Input: Decodable, Output: TemplateRepresentable >: FormComponent { public var key: String public var input: Input public var output: Output public var error: String? public init( key: String, input: Input, output: Output, error: String? = nil ) { self.key = key self.input = input self.output = output self.error = error } open func config( _ block: (AbstractFormField) -> Void ) -> Self { block(self) return self } open func load(req: Vapor.Request) async throws { } open func process(req: Vapor.Request) async throws { if let value = try? req.content.get(Input.self, at: key) { input = value } } open func validate(req: Vapor.Request) async throws -> Bool { true } open func write(req: Vapor.Request) async throws { } open func save(req: Vapor.Request) async throws { } open func read(req: Vapor.Request) async throws { } open func render(req: Vapor.Request) -> TemplateRepresentable { output } } ================================================ FILE: Chapter 06/myProject/Sources/App/Framework/Form/Fields/InputField.swift ================================================ public final class InputField: AbstractFormField< String, InputFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init( key: key ) ) ) } } ================================================ FILE: Chapter 06/myProject/Sources/App/Framework/Form/FormAction.swift ================================================ import SwiftHtml public struct FormAction { public var method: SwiftHtml.Method public var url: String? public var enctype: SwiftHtml.Enctype? public init( method: SwiftHtml.Method = .post, url: String? = nil, enctype: SwiftHtml.Enctype? = nil ) { self.method = method self.url = url self.enctype = enctype } } ================================================ FILE: Chapter 06/myProject/Sources/App/Framework/Form/FormComponent.swift ================================================ import Vapor public protocol FormComponent { func load(req: Request) async throws func process(req: Request) async throws func validate(req: Request) async throws -> Bool func write(req: Request) async throws func save(req: Request) async throws func read(req: Request) async throws func render(req: Request) -> TemplateRepresentable } ================================================ FILE: Chapter 06/myProject/Sources/App/Framework/Form/FormComponentBuilder.swift ================================================ @resultBuilder public enum FormComponentBuilder { public static func buildBlock( _ components: FormComponent... ) -> [FormComponent] { components } } ================================================ FILE: Chapter 06/myProject/Sources/App/Framework/Form/Templates/Contexts/FormContext.swift ================================================ public struct FormContext { public var action: FormAction public var fields: [TemplateRepresentable] public var error: String? public var submit: String? } ================================================ FILE: Chapter 06/myProject/Sources/App/Framework/Form/Templates/Contexts/InputFieldContext.swift ================================================ import SwiftHtml public struct InputFieldContext { public let key: String public var label: LabelContext public var type: SwiftHtml.Input.`Type` public var placeholder: String? public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, type: Input.`Type` = .text, placeholder: String? = nil, value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.type = type self.placeholder = placeholder self.value = value self.error = error } } ================================================ FILE: Chapter 06/myProject/Sources/App/Framework/Form/Templates/Contexts/LabelContext.swift ================================================ public struct LabelContext { public var key: String public var title: String? public var required: Bool public var more: String? public init( key: String, title: String? = nil, required: Bool = false, more: String? = nil ) { self.key = key self.title = title self.required = required self.more = more } } ================================================ FILE: Chapter 06/myProject/Sources/App/Framework/Form/Templates/Html/FormTemplate.swift ================================================ import Vapor import SwiftHtml public struct FormTemplate: TemplateRepresentable { var context: FormContext public init( _ context: FormContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Form { if let error = context.error { Section { P(error) .class("error") } } for field in context.fields { Section { field.render(req) } } Section { Input() .type(.submit) .value(context.submit ?? "Save") } } .method(context.action.method) .action(context.action.url) .enctype(context.action.enctype) } } ================================================ FILE: Chapter 06/myProject/Sources/App/Framework/Form/Templates/Html/InputFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct InputFieldTemplate: TemplateRepresentable { public var context: InputFieldContext public init( _ context: InputFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Input() .type(context.type) .key(context.key) .placeholder(context.placeholder) .value(context.value) .class("field") if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 06/myProject/Sources/App/Framework/Form/Templates/Html/LabelTemplate.swift ================================================ import Vapor import SwiftHtml public struct LabelTemplate: TemplateRepresentable { var context: LabelContext public init( _ context: LabelContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Label { Text(context.title ?? context.key.capitalized) if let more = context.more { Span(more) .class("more") } if context.required { Span("*") .class("required") } }.for(context.key) } } ================================================ FILE: Chapter 06/myProject/Sources/App/Framework/ModuleInterface.swift ================================================ import Vapor public protocol ModuleInterface { static var identifier: String { get } func boot(_ app: Application) throws } public extension ModuleInterface { func boot(_ app: Application) throws {} static var identifier: String { String(describing: self).dropLast(6).lowercased() } } ================================================ FILE: Chapter 06/myProject/Sources/App/Middlewares/ExtendPathMiddleware.swift ================================================ import Vapor struct ExtendPathMiddleware: AsyncMiddleware { func respond( to req: Request, chainingTo next: AsyncResponder ) async throws -> Response { if !req.url.path.hasSuffix("/") && !req.url.path.contains(".") { return req.redirect( to: req.url.path + "/", redirectType: .permanent ) } return try await next.respond( to: req ) } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/Blog/BlogModule.swift ================================================ import Vapor struct BlogModule: ModuleInterface { let router = BlogRouter() func boot(_ app: Application) throws { app.migrations.add(BlogMigrations.v1()) app.migrations.add(BlogMigrations.seed()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/Blog/BlogRouter.swift ================================================ import Vapor struct BlogRouter: RouteCollection { let controller = BlogFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get("blog", use: controller.blogView) routes.get(.anything, use: controller.postView) } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift ================================================ import Vapor import Fluent struct BlogFrontendController { func blogView(req: Request) async throws -> Response { let postModels = try await BlogPostModel .query(on: req.db) .sort(\.$date, .descending) .all() let posts = try postModels.map { Blog.Post.List( id: try $0.requireID(), title: $0.title, slug: $0.slug, image: $0.imageKey, excerpt: $0.excerpt, date: $0.date ) } let ctx = BlogPostsContext( icon: "🔥", title: "Blog", message: "Hot news and stories about everything.", posts: posts) return req.templates.renderHtml( BlogPostsTemplate(ctx) ) } func postView(req: Request) async throws -> Response { let slug = req.url.path.trimmingCharacters( in: .init(charactersIn: "/") ) guard let post = try await BlogPostModel .query(on: req.db) .filter(\.$slug == slug) .with(\.$category) .first() else { return req.redirect(to: "/") } let ctx = BlogPostContext( post: Blog.Post.Detail( id: post.id!, title: post.title, slug: post.slug, image: post.imageKey, excerpt: post.excerpt, date: post.date, category: .init( id: post.category.id!, title: post.category.title ), content: post.content ) ) return req.templates.renderHtml( BlogPostTemplate(ctx) ) } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/Blog/Database/Migrations/BlogMigrations.swift ================================================ import Foundation import Fluent enum BlogMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema) .id() .field(BlogCategoryModel.FieldKeys.v1.title, .string, .required) .create() try await db.schema(BlogPostModel.schema) .id() .field(BlogPostModel.FieldKeys.v1.title, .string, .required) .field(BlogPostModel.FieldKeys.v1.slug, .string, .required) .field(BlogPostModel.FieldKeys.v1.imageKey, .string, .required) .field(BlogPostModel.FieldKeys.v1.excerpt, .data, .required) .field(BlogPostModel.FieldKeys.v1.date, .datetime, .required) .field(BlogPostModel.FieldKeys.v1.content, .data, .required) .field(BlogPostModel.FieldKeys.v1.categoryId, .uuid) .foreignKey( BlogPostModel.FieldKeys.v1.categoryId, references: BlogCategoryModel.schema, .id, onDelete: DatabaseSchema.ForeignKeyAction.setNull, onUpdate: .cascade ) .unique(on: BlogPostModel.FieldKeys.v1.slug) .create() } func revert(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema).delete() try await db.schema(BlogPostModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let categories = (1...4).map { index in BlogCategoryModel(title: "Sample category #\(index)") } try await categories.create(on: db) try await (1...9).map { index in BlogPostModel( id: nil, title: "Sample post #\(index)", slug: "sample-post-\(index)", imageKey: "/img/posts/\(String(format: "%02d", index + 1)).jpg", excerpt: "Lorem ipsum", date: Date().addingTimeInterval(-Double.random(in: 0...(86400 * 60))), content: "Lorem ipsum dolor sit amet.", categoryId: categories[Int.random(in: 0.. Tag { WebIndexTemplate( .init(title: context.post.title) ) { Div { Section { P(dateFormatter.string(from: context.post.date)) H1(context.post.title) P(context.post.excerpt) } .class(["lead", "container"]) Img(src: context.post.image, alt: context.post.title) Article { Text(context.post.content) } .class("container") } .id("post") } .render(req) } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostsTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostsTemplate: TemplateRepresentable { var context: BlogPostsContext init( _ context: BlogPostsContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") Div { for post in context.posts { Article { A { Img(src: post.image, alt: post.title) H2(post.title) P(post.excerpt) } .href("/\(post.slug)/") } } } .class("grid-221") } .id("blog") } .render(req) } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/User/Authenticators/UserCredentialsAuthenticator.swift ================================================ import Vapor import Fluent struct UserCredentialsAuthenticator: AsyncCredentialsAuthenticator { struct Credentials: Content { let email: String let password: String } func authenticate( credentials: Credentials, for req: Request ) async throws { guard let user = try await UserAccountModel .query(on: req.db) .filter(\.$email == credentials.email) .first() else { return } do { guard try Bcrypt.verify( credentials.password, created: user.password ) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } catch { // do nothing... } } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/User/Authenticators/UserSessionAuthenticator.swift ================================================ import Vapor import Fluent struct UserSessionAuthenticator: AsyncSessionAuthenticator { typealias User = AuthenticatedUser func authenticate( sessionID: User.SessionID, for req: Request ) async throws { guard let user = try await UserAccountModel .find(sessionID, on: req.db) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/User/Controllers/UserFrontendController.swift ================================================ import Vapor struct UserFrontendController { private func renderSignInView( _ req: Request, _ form: UserLoginForm ) -> Response { let template = UserLoginTemplate( .init( icon: "⬇️", title: "Sign in", message: "Please log in with your existing account", form: form.render(req: req) ) ) return req.templates.renderHtml(template) } func signInView( _ req: Request ) async throws -> Response { renderSignInView(req, .init()) } func signInAction( _ req: Request ) async throws -> Response { /// the user is authenticated, we can store the user data inside the session too if let user = req.auth.get(AuthenticatedUser.self) { req.session.authenticate(user) return req.redirect(to: "/") } let form = UserLoginForm() try await form.process(req: req) form.error = "Invalid email or password." return renderSignInView(req, form) } func signOut( _ req: Request ) throws -> Response { req.auth.logout(AuthenticatedUser.self) req.session.unauthenticate(AuthenticatedUser.self) return req.redirect(to: "/") } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/User/Database/Migrations/UserMigrations.swift ================================================ import Vapor import Fluent enum UserMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(UserAccountModel.schema) .id() .field(UserAccountModel.FieldKeys.v1.email, .string, .required) .field(UserAccountModel.FieldKeys.v1.password, .string, .required) .unique(on: UserAccountModel.FieldKeys.v1.email) .create() } func revert(on db: Database) async throws { try await db.schema(UserAccountModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let email = "root@localhost.com" let password = "ChangeMe1" let user = UserAccountModel( email: email, password: try Bcrypt.hash(password) ) try await user.create(on: db) } func revert(on db: Database) async throws { try await UserAccountModel.query(on: db).delete() } } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/User/Database/Models/UserAccountModel.swift ================================================ import Vapor import Fluent final class UserAccountModel: DatabaseModelInterface { typealias Module = UserModule struct FieldKeys { struct v1 { static var email: FieldKey { "email" } static var password: FieldKey { "password" } } } @ID() var id: UUID? @Field(key: FieldKeys.v1.email) var email: String @Field(key: FieldKeys.v1.password) var password: String init() { } init( id: UUID? = nil, email: String, password: String ) { self.id = id self.email = email self.password = password } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/User/Forms/UserLoginForm.swift ================================================ import Vapor final class UserLoginForm: AbstractForm { public convenience init() { self.init( action: .init( method: .post, url: "/sign-in/" ), submit: "Sign in" ) self.fields = createFields() } @FormComponentBuilder func createFields() -> [FormComponent] { InputField("email") .config { $0.output.context.label.required = true $0.output.context.type = .email } InputField("password") .config { $0.output.context.label.required = true $0.output.context.type = .password } } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/User/Templates/Contexts/UserLoginContext.swift ================================================ struct UserLoginContext { let icon: String let title: String let message: String let form: TemplateRepresentable init( icon: String, title: String, message: String, form: TemplateRepresentable ) { self.icon = icon self.title = title self.message = message self.form = form } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/User/Templates/Html/UserLoginTemplate.swift ================================================ import Vapor import SwiftHtml struct UserLoginTemplate: TemplateRepresentable { var context: UserLoginContext init( _ context: UserLoginContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") context.form.render(req) } .id("user-login") .class("container") } .render(req) } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/User/UserModule.swift ================================================ import Vapor struct UserModule: ModuleInterface { let router = UserRouter() func boot(_ app: Application) throws { app.migrations.add(UserMigrations.v1()) app.migrations.add(UserMigrations.seed()) app.middleware.use(UserSessionAuthenticator()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/User/UserRouter.swift ================================================ import Vapor struct UserRouter: RouteCollection { let frontendController = UserFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get("sign-in", use: frontendController.signInView) routes .grouped( UserCredentialsAuthenticator() ) .post("sign-in", use: frontendController.signInAction) routes.get("sign-out", use: frontendController.signOut) } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/Web/Controllers/WebFrontendController.swift ================================================ import Vapor struct WebFrontendController { func homeView(req: Request) throws -> Response { let ctx = WebHomeContext( icon: "👋", title: "Home", message: "Hi there, welcome to my page.", paragraphs: [ "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.", "Nisi ut aliquip ex ea commodo consequat.", ], link: .init( label: "Read my blog →", url: "/blog/" ) ) return req.templates.renderHtml( WebHomeTemplate(ctx) ) } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/Web/Templates/Contexts/WebHomeContext.swift ================================================ struct WebHomeContext { let icon: String let title: String let message: String let paragraphs: [String] let link: WebLinkContext } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/Web/Templates/Contexts/WebIndexContext.swift ================================================ public struct WebIndexContext { public let title: String public init( title: String ) { self.title = title } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/Web/Templates/Contexts/WebLinkContext.swift ================================================ public struct WebLinkContext { public let label: String public let url: String public init( label: String, url: String ) { self.label = label self.url = url } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/Web/Templates/Html/WebHomeTemplate.swift ================================================ import Vapor import SwiftHtml struct WebHomeTemplate: TemplateRepresentable { var context: WebHomeContext init( _ context: WebHomeContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") for paragraph in context.paragraphs { P(paragraph) } WebLinkTemplate(context.link).render(req) } .id("home") .class("container") } .render(req) } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg extension Svg { static func menuIcon() -> Svg { Svg { Line(x1: 3, y1: 12, x2: 21, y2: 12) Line(x1: 3, y1: 6, x2: 21, y2: 6) Line(x1: 3, y1: 18, x2: 21, y2: 18) } .width(24) .height(24) .viewBox(minX: 0, minY: 0, width: 24, height: 24) .fill("none") .stroke("currentColor") .strokeWidth(2) .strokeLinecap("round") .strokeLinejoin("round") } } public struct WebIndexTemplate: TemplateRepresentable { public var context: WebIndexContext var body: Tag public init( _ context: WebIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Link(rel: .shortcutIcon) .href("/img/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css") Link(rel: .stylesheet) .href("/css/web.css") Title(context.title) } Body { Header { Div { A { Img(src: "/img/logo.png", alt: "Logo") } .id("site-logo") .href("/") Nav { Input() .type(.checkbox) .id("primary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("primary-menu-button") Div { A("Home") .href("/") .class("selected", req.url.path == "/") A("Blog") .href("/blog/") .class("selected", req.url.path == "/blog/") A("About") .href("#") .onClick("javascript:about();") if req.auth.has(AuthenticatedUser.self) { A("Sign out") .href("/sign-out/") } else { A("Sign in") .href("/sign-in/") } } .class("menu-items") } .id("primary-menu") } .id("navigation") } Main { body } Footer { Section { P { Text("This site is powered by ") A("Swift") .href("https://swift.org") .target(.blank) Text(" & ") A("Vapor") .href("https://vapor.codes") .target(.blank) Text(".") } P("myPage © 2020-2022") } } Script() .type(.javascript) .src("/js/web.js") } } .lang("en-US") } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/Web/Templates/Html/WebLinkTemplate.swift ================================================ import Vapor import SwiftHtml struct WebLinkTemplate: TemplateRepresentable { var context: WebLinkContext init( _ context: WebLinkContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { A(context.label) .href(context.url) } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/Web/WebModule.swift ================================================ import Vapor struct WebModule: ModuleInterface { let router = WebRouter() func boot(_ app: Application) throws { try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 06/myProject/Sources/App/Modules/Web/WebRouter.swift ================================================ import Vapor struct WebRouter: RouteCollection { let frontendController = WebFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get(use: frontendController.homeView) } } ================================================ FILE: Chapter 06/myProject/Sources/App/Template/Request+Template.swift ================================================ import Vapor public extension Request { var templates: TemplateRenderer { .init(self) } } ================================================ FILE: Chapter 06/myProject/Sources/App/Template/TemplateRenderer.swift ================================================ import Vapor import SwiftHtml public struct TemplateRenderer { var req: Request init(_ req: Request) { self.req = req } public func renderHtml( _ template: TemplateRepresentable, minify: Bool = false, indent: Int = 4 ) -> Response { let doc = Document(.html) { template.render(req) } let body = DocumentRenderer( minify: minify, indent: indent ) .render(doc) return Response( status: .ok, headers: [ "content-type": "text/html" ], body: .init(string: body) ) } } ================================================ FILE: Chapter 06/myProject/Sources/App/Template/TemplateRepresentable.swift ================================================ import Vapor import SwiftSgml public protocol TemplateRepresentable { @TagBuilder func render(_ req: Request) -> Tag } ================================================ FILE: Chapter 06/myProject/Sources/App/configure.swift ================================================ import Vapor import Fluent import FluentSQLiteDriver public func configure( _ app: Application ) throws { let dbPath = app.directory.resourcesDirectory + "db.sqlite" app.databases.use(.sqlite(.file(dbPath)), as: .sqlite) app.middleware.use( FileMiddleware( publicDirectory: app.directory.publicDirectory ) ) app.middleware.use(ExtendPathMiddleware()) app.sessions.use(.fluent) app.migrations.add(SessionRecord.migration) app.middleware.use(app.sessions.middleware) let modules: [ModuleInterface] = [ WebModule(), UserModule(), BlogModule(), ] for module in modules { try module.boot(app) } try app.autoMigrate().wait() } ================================================ FILE: Chapter 06/myProject/Sources/App/routes.swift ================================================ import Vapor import SwiftHtml func routes(_ app: Application) throws { app.routes.get { req -> Response in req.templates.renderHtml( WebIndexTemplate( WebIndexContext( title: "Home" ) ) { P("Hi there, welcome to my page!") } ) } } ================================================ FILE: Chapter 06/myProject/Sources/Run/main.swift ================================================ import App import Vapor var env = try Environment.detect() try LoggingSystem.bootstrap(from: &env) let app = Application(env) defer { app.shutdown() } try configure(app) try app.run() ================================================ FILE: Chapter 06/myProject/Tests/AppTests/AppTests.swift ================================================ @testable import App import XCTVapor final class AppTests: XCTestCase { func testHelloWorld() throws { let app = Application(.testing) defer { app.shutdown() } try configure(app) try app.test(.GET, "hello", afterResponse: { res in XCTAssertEqual(res.status, .ok) XCTAssertEqual(res.body.string, "Hello, world!") }) } } ================================================ FILE: Chapter 06/myProject/docker-compose.yml ================================================ # 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 # Stop all: docker compose down # x-shared_environment: &shared_environment LOG_LEVEL: ${LOG_LEVEL:-debug} services: app: image: myproject:latest build: context: . environment: <<: *shared_environment 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"] ================================================ FILE: Chapter 07/myProject/Dockerfile ================================================ # ================================ # Build image # ================================ FROM swift:6.0-noble 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 \ $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true) # Copy entire repo into container COPY . . # Build the application, 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 \ --product myProject \ --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)/myProject" ./ # 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:noble # 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 ["./myProject"] CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] ================================================ FILE: Chapter 07/myProject/Package.swift ================================================ // swift-tools-version:5.7 import PackageDescription let package = Package( name: "myProject", platforms: [ .macOS(.v12) ], dependencies: [ .package( url: "https://github.com/vapor/vapor", from: "4.70.0" ), .package( url: "https://github.com/vapor/fluent", from: "4.4.0" ), .package( url: "https://github.com/vapor/fluent-sqlite-driver", from: "4.1.0" ), .package( url: "https://github.com/binarybirds/swift-html", from: "1.7.0" ), ], targets: [ .target(name: "App", dependencies: [ .product(name: "Vapor", package: "vapor"), .product(name: "Fluent", package: "fluent"), .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), .product(name: "SwiftHtml", package: "swift-html"), .product(name: "SwiftSvg", package: "swift-html"), ]), .executableTarget(name: "Run", dependencies: ["App"]), .testTarget(name: "AppTests", dependencies: [ .target(name: "App"), .product(name: "XCTVapor", package: "vapor"), ]) ] ) ================================================ FILE: Chapter 07/myProject/Public/css/web.css ================================================ #blog h2 { margin: 0.5rem 0; } ================================================ FILE: Chapter 07/myProject/Public/js/web.js ================================================ function about() { alert("myPage\n\nversion 1.0.0"); } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/AuthenticatedUser.swift ================================================ import Vapor public struct AuthenticatedUser { public let id: UUID public let email: String public init( id: UUID, email: String ) { self.id = id self.email = email } } extension AuthenticatedUser: SessionAuthenticatable { public var sessionID: UUID { id } } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/DatabaseModelInterface.swift ================================================ import Vapor import Fluent public protocol DatabaseModelInterface: Fluent.Model where Self.IDValue == UUID { associatedtype Module: ModuleInterface static var identifier: String { get } } public extension DatabaseModelInterface { static var schema: String { Module.identifier + "_" + identifier } static var identifier: String { String(describing: self) .dropFirst(Module.identifier.count) .dropLast(5) .lowercased() + "s" } } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/Form/AbstractForm.swift ================================================ import Vapor open class AbstractForm: FormComponent { open var action: FormAction open var fields: [FormComponent] open var error: String? open var submit: String? public init( action: FormAction = .init(), fields: [FormComponent] = [], error: String? = nil, submit: String? = nil ) { self.action = action self.fields = fields self.error = error self.submit = submit self.action.enctype = .multipart } open func load(req: Request) async throws { for field in fields { try await field.load(req: req) } } open func process(req: Request) async throws { for field in fields { try await field.process(req: req) } } open func validate(req: Request) async throws -> Bool { var result: [Bool] = [] for field in fields { result.append(try await field.validate(req: req)) } return result.filter { $0 == false }.isEmpty } open func write(req: Request) async throws { for field in fields { try await field.write(req: req) } } open func save(req: Request) async throws { for field in fields { try await field.save(req: req) } } open func read(req: Request) async throws { for field in fields { try await field.read(req: req) } } open func render(req: Request) -> TemplateRepresentable { FormTemplate( .init( action: action, fields: fields.map { $0.render(req: req)}, error: error, submit: submit ) ) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/Form/AbstractFormField.swift ================================================ import Vapor open class AbstractFormField< Input: Decodable, Output: TemplateRepresentable >: FormComponent { public var key: String public var input: Input public var output: Output public var error: String? // MARK: - event blocks public typealias FormFieldBlock = (Request, AbstractFormField) async throws -> Void public typealias FormFieldValidatorsBlock = ((Request, AbstractFormField) -> [AsyncValidator]) private var readBlock: FormFieldBlock? private var writeBlock: FormFieldBlock? private var loadBlock: FormFieldBlock? private var saveBlock: FormFieldBlock? private var validatorsBlock: FormFieldValidatorsBlock? // MARK: - init & config public init( key: String, input: Input, output: Output, error: String? = nil ) { self.key = key self.input = input self.output = output self.error = error } open func config( _ block: (AbstractFormField) -> Void ) -> Self { block(self) return self } // MARK: - Block setters open func read(_ block: @escaping FormFieldBlock) -> Self { readBlock = block return self } open func write(_ block: @escaping FormFieldBlock) -> Self { writeBlock = block return self } open func load(_ block: @escaping FormFieldBlock) -> Self { loadBlock = block return self } open func save(_ block: @escaping FormFieldBlock) -> Self { saveBlock = block return self } open func validators( @AsyncValidatorBuilder _ block: @escaping FormFieldValidatorsBlock ) -> Self { validatorsBlock = block return self } // MARK: - FormComponent open func process(req: Request) async throws { if let value = try? req.content.get(Input.self, at: key) { input = value } } open func validate(req: Request) async throws -> Bool { guard let validators = validatorsBlock else { return true } return await RequestValidator(validators(req, self)).isValid(req) } open func read(req: Request) async throws { try await readBlock?(req, self) } open func write(req: Request) async throws { try await writeBlock?(req, self) } open func load(req: Request) async throws { try await loadBlock?(req, self) } open func save(req: Request) async throws { try await saveBlock?(req, self) } open func render(req: Request) -> TemplateRepresentable { output } } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/Form/Fields/InputField.swift ================================================ public final class InputField: AbstractFormField< String, InputFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init( key: key ) ) ) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/Form/FormAction.swift ================================================ import SwiftHtml public struct FormAction { public var method: SwiftHtml.Method public var url: String? public var enctype: SwiftHtml.Enctype? public init( method: SwiftHtml.Method = .post, url: String? = nil, enctype: SwiftHtml.Enctype? = nil ) { self.method = method self.url = url self.enctype = enctype } } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/Form/FormComponent.swift ================================================ import Vapor public protocol FormComponent { func load(req: Request) async throws func process(req: Request) async throws func validate(req: Request) async throws -> Bool func write(req: Request) async throws func save(req: Request) async throws func read(req: Request) async throws func render(req: Request) -> TemplateRepresentable } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/Form/FormComponentBuilder.swift ================================================ @resultBuilder public enum FormComponentBuilder { public static func buildBlock( _ components: FormComponent... ) -> [FormComponent] { components } } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/Form/Templates/Contexts/FormContext.swift ================================================ public struct FormContext { public var action: FormAction public var fields: [TemplateRepresentable] public var error: String? public var submit: String? } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/Form/Templates/Contexts/InputFieldContext.swift ================================================ import SwiftHtml public struct InputFieldContext { public let key: String public var label: LabelContext public var type: SwiftHtml.Input.`Type` public var placeholder: String? public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, type: Input.`Type` = .text, placeholder: String? = nil, value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.type = type self.placeholder = placeholder self.value = value self.error = error } } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/Form/Templates/Contexts/LabelContext.swift ================================================ public struct LabelContext { public var key: String public var title: String? public var required: Bool public var more: String? public init( key: String, title: String? = nil, required: Bool = false, more: String? = nil ) { self.key = key self.title = title self.required = required self.more = more } } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/Form/Templates/Html/FormTemplate.swift ================================================ import Vapor import SwiftHtml public struct FormTemplate: TemplateRepresentable { var context: FormContext public init( _ context: FormContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Form { if let error = context.error { Section { P(error) .class("error") } } for field in context.fields { Section { field.render(req) } } Section { Input() .type(.submit) .value(context.submit ?? "Save") } } .method(context.action.method) .action(context.action.url) .enctype(context.action.enctype) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/Form/Templates/Html/InputFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct InputFieldTemplate: TemplateRepresentable { public var context: InputFieldContext public init( _ context: InputFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Input() .type(context.type) .key(context.key) .placeholder(context.placeholder) .value(context.value) .class("field") if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/Form/Templates/Html/LabelTemplate.swift ================================================ import Vapor import SwiftHtml public struct LabelTemplate: TemplateRepresentable { var context: LabelContext public init( _ context: LabelContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Label { Text(context.title ?? context.key.capitalized) if let more = context.more { Span(more) .class("more") } if context.required { Span("*") .class("required") } }.for(context.key) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/ModuleInterface.swift ================================================ import Vapor public protocol ModuleInterface { static var identifier: String { get } func boot(_ app: Application) throws } public extension ModuleInterface { func boot(_ app: Application) throws {} static var identifier: String { String(describing: self).dropLast(6).lowercased() } } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/Validation/AsyncValidator.swift ================================================ import Vapor public protocol AsyncValidator { var key: String { get } var message: String { get } func validate( _ req: Request ) async throws -> ValidationErrorDetail? } public extension AsyncValidator { var error: ValidationErrorDetail { .init(key: key, message: message) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/Validation/AsyncValidatorBuilder.swift ================================================ @resultBuilder public enum AsyncValidatorBuilder { public static func buildBlock( _ components: AsyncValidator... ) -> [AsyncValidator] { components } } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/Validation/FormFieldValidator+Validations.swift ================================================ import Vapor public extension FormFieldValidator where Input == String { static func required( _ field: AbstractFormField, _ message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is required" return .init(field, msg) { _, field in !field.input.isEmpty } } static func min( _ field: AbstractFormField, length: Int, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)" return .init(field, msg) { _, field in field.input.count >= length } } static func max( _ field: AbstractFormField, length: Int, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)" return .init(field, msg) { _, field in field.input.count <= length } } static func alphanumeric( _ field: AbstractFormField, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) should be only alphanumeric characters" return .init(field, msg) { _, field in !Validator .characterSet(.alphanumerics) .validate(field.input) .isFailure } } static func email( _ field: AbstractFormField, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) should be a valid email address" return .init(field, msg) { _, field in !Validator .email .validate(field.input) .isFailure } } } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/Validation/FormFieldValidator.swift ================================================ import Vapor public struct FormFieldValidator< Input: Decodable, Output: TemplateRepresentable >: AsyncValidator { public typealias AsyncValidationBlock = ((Request, AbstractFormField) async throws -> Bool) public let field: AbstractFormField public let message: String public let validation: AsyncValidationBlock public var key: String { field.key } public init( _ field: AbstractFormField, _ message: String, _ validation: @escaping AsyncValidationBlock ) { self.field = field self.message = message self.validation = validation } public func validate( _ req: Request ) async throws -> ValidationErrorDetail? { let isValid = try await validation(req, field) if isValid { return nil } field.error = message return error } } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/Validation/RequestValidator.swift ================================================ import Vapor public struct RequestValidator { public var validators: [AsyncValidator] public init( _ validators: [AsyncValidator] ) { self.validators = validators } public func validate( _ req: Request, message: String? = nil ) async throws { var result: [ValidationErrorDetail] = [] for validator in validators { if result.contains(where: { $0.key == validator.key }) { continue } if let res = try await validator.validate(req) { result.append(res) } } if !result.isEmpty { throw ValidationAbort( abort: Abort(.badRequest, reason: message), details: result ) } } public func isValid( _ req: Request ) async -> Bool { do { try await validate(req, message: nil) return true } catch { return false } } } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/Validation/ValidationAbort.swift ================================================ import Vapor public struct ValidationAbort: AbortError { public var abort: Abort public var message: String? public var details: [ValidationErrorDetail] public var reason: String { abort.reason } public var status: HTTPStatus { abort.status } public init( abort: Abort, message: String? = nil, details: [ValidationErrorDetail] ) { self.abort = abort self.message = message self.details = details } } ================================================ FILE: Chapter 07/myProject/Sources/App/Framework/Validation/ValidationErrorDetail.swift ================================================ import Vapor public struct ValidationErrorDetail: Codable { public var key: String public var message: String public init( key: String, message: String ) { self.key = key self.message = message } } extension ValidationErrorDetail: Content {} ================================================ FILE: Chapter 07/myProject/Sources/App/Middlewares/ExtendPathMiddleware.swift ================================================ import Vapor struct ExtendPathMiddleware: AsyncMiddleware { func respond( to req: Request, chainingTo next: AsyncResponder ) async throws -> Response { if !req.url.path.hasSuffix("/") && !req.url.path.contains(".") { return req.redirect( to: req.url.path + "/", redirectType: .permanent ) } return try await next.respond( to: req ) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/Blog/BlogModule.swift ================================================ import Vapor struct BlogModule: ModuleInterface { let router = BlogRouter() func boot(_ app: Application) throws { app.migrations.add(BlogMigrations.v1()) app.migrations.add(BlogMigrations.seed()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/Blog/BlogRouter.swift ================================================ import Vapor struct BlogRouter: RouteCollection { let controller = BlogFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get("blog", use: controller.blogView) routes.get(.anything, use: controller.postView) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift ================================================ import Vapor import Fluent struct BlogFrontendController { func blogView(req: Request) async throws -> Response { let postModels = try await BlogPostModel .query(on: req.db) .sort(\.$date, .descending) .all() let posts = try postModels.map { Blog.Post.List( id: try $0.requireID(), title: $0.title, slug: $0.slug, image: $0.imageKey, excerpt: $0.excerpt, date: $0.date ) } let ctx = BlogPostsContext( icon: "🔥", title: "Blog", message: "Hot news and stories about everything.", posts: posts) return req.templates.renderHtml( BlogPostsTemplate(ctx) ) } func postView(req: Request) async throws -> Response { let slug = req.url.path.trimmingCharacters( in: .init(charactersIn: "/") ) guard let post = try await BlogPostModel .query(on: req.db) .filter(\.$slug == slug) .with(\.$category) .first() else { return req.redirect(to: "/") } let ctx = BlogPostContext( post: Blog.Post.Detail( id: post.id!, title: post.title, slug: post.slug, image: post.imageKey, excerpt: post.excerpt, date: post.date, category: .init( id: post.category.id!, title: post.category.title ), content: post.content ) ) return req.templates.renderHtml( BlogPostTemplate(ctx) ) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/Blog/Database/Migrations/BlogMigrations.swift ================================================ import Foundation import Fluent enum BlogMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema) .id() .field(BlogCategoryModel.FieldKeys.v1.title, .string, .required) .create() try await db.schema(BlogPostModel.schema) .id() .field(BlogPostModel.FieldKeys.v1.title, .string, .required) .field(BlogPostModel.FieldKeys.v1.slug, .string, .required) .field(BlogPostModel.FieldKeys.v1.imageKey, .string, .required) .field(BlogPostModel.FieldKeys.v1.excerpt, .data, .required) .field(BlogPostModel.FieldKeys.v1.date, .datetime, .required) .field(BlogPostModel.FieldKeys.v1.content, .data, .required) .field(BlogPostModel.FieldKeys.v1.categoryId, .uuid) .foreignKey( BlogPostModel.FieldKeys.v1.categoryId, references: BlogCategoryModel.schema, .id, onDelete: DatabaseSchema.ForeignKeyAction.setNull, onUpdate: .cascade ) .unique(on: BlogPostModel.FieldKeys.v1.slug) .create() } func revert(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema).delete() try await db.schema(BlogPostModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let categories = (1...4).map { index in BlogCategoryModel(title: "Sample category #\(index)") } try await categories.create(on: db) try await (1...9).map { index in BlogPostModel( id: nil, title: "Sample post #\(index)", slug: "sample-post-\(index)", imageKey: "/img/posts/\(String(format: "%02d", index + 1)).jpg", excerpt: "Lorem ipsum", date: Date().addingTimeInterval(-Double.random(in: 0...(86400 * 60))), content: "Lorem ipsum dolor sit amet.", categoryId: categories[Int.random(in: 0.. Tag { WebIndexTemplate( .init(title: context.post.title) ) { Div { Section { P(dateFormatter.string(from: context.post.date)) H1(context.post.title) P(context.post.excerpt) } .class(["lead", "container"]) Img(src: context.post.image, alt: context.post.title) Article { Text(context.post.content) } .class("container") } .id("post") } .render(req) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostsTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostsTemplate: TemplateRepresentable { var context: BlogPostsContext init( _ context: BlogPostsContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") Div { for post in context.posts { Article { A { Img(src: post.image, alt: post.title) H2(post.title) P(post.excerpt) } .href("/\(post.slug)/") } } } .class("grid-221") } .id("blog") } .render(req) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/User/Authenticators/UserCredentialsAuthenticator.swift ================================================ import Vapor import Fluent struct UserCredentialsAuthenticator: AsyncCredentialsAuthenticator { struct Credentials: Content { let email: String let password: String } func authenticate( credentials: Credentials, for req: Request ) async throws { guard let user = try await UserAccountModel .query(on: req.db) .filter(\.$email == credentials.email) .first() else { return } do { guard try Bcrypt.verify( credentials.password, created: user.password ) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } catch { // do nothing... } } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/User/Authenticators/UserSessionAuthenticator.swift ================================================ import Vapor import Fluent struct UserSessionAuthenticator: AsyncSessionAuthenticator { typealias User = AuthenticatedUser func authenticate( sessionID: User.SessionID, for req: Request ) async throws { guard let user = try await UserAccountModel .find(sessionID, on: req.db) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/User/Controllers/UserFrontendController.swift ================================================ import Vapor struct UserFrontendController { private func renderSignInView( _ req: Request, _ form: UserLoginForm ) -> Response { let template = UserLoginTemplate( .init( icon: "⬇️", title: "Sign in", message: "Please log in with your existing account", form: form.render(req: req) ) ) return req.templates.renderHtml(template) } func signInView( _ req: Request ) async throws -> Response { renderSignInView(req, .init()) } func signInAction( _ req: Request ) async throws -> Response { /// the user is authenticated, we can store the user data inside the session too if let user = req.auth.get(AuthenticatedUser.self) { req.session.authenticate(user) return req.redirect(to: "/") } let form = UserLoginForm() try await form.process(req: req) let isValid = try await form.validate(req: req) if !isValid { form.error = "Invalid email or password." } return renderSignInView(req, form) } func signOut( _ req: Request ) throws -> Response { req.auth.logout(AuthenticatedUser.self) req.session.unauthenticate(AuthenticatedUser.self) return req.redirect(to: "/") } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/User/Database/Migrations/UserMigrations.swift ================================================ import Vapor import Fluent enum UserMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(UserAccountModel.schema) .id() .field(UserAccountModel.FieldKeys.v1.email, .string, .required) .field(UserAccountModel.FieldKeys.v1.password, .string, .required) .unique(on: UserAccountModel.FieldKeys.v1.email) .create() } func revert(on db: Database) async throws { try await db.schema(UserAccountModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let email = "root@localhost.com" let password = "ChangeMe1" let user = UserAccountModel( email: email, password: try Bcrypt.hash(password) ) try await user.create(on: db) } func revert(on db: Database) async throws { try await UserAccountModel.query(on: db).delete() } } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/User/Database/Models/UserAccountModel.swift ================================================ import Vapor import Fluent final class UserAccountModel: DatabaseModelInterface { typealias Module = UserModule struct FieldKeys { struct v1 { static var email: FieldKey { "email" } static var password: FieldKey { "password" } } } @ID() var id: UUID? @Field(key: FieldKeys.v1.email) var email: String @Field(key: FieldKeys.v1.password) var password: String init() { } init( id: UUID? = nil, email: String, password: String ) { self.id = id self.email = email self.password = password } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/User/Forms/UserLoginForm.swift ================================================ import Vapor final class UserLoginForm: AbstractForm { public convenience init() { self.init( action: .init( method: .post, url: "/sign-in/" ), submit: "Sign in" ) self.fields = createFields() } @FormComponentBuilder func createFields() -> [FormComponent] { InputField("email") .config { $0.output.context.label.required = true $0.output.context.type = .email } .validators { FormFieldValidator.required($1) FormFieldValidator.email($1) } InputField("password") .config { $0.output.context.label.required = true $0.output.context.type = .password } .validators { FormFieldValidator.required($1) } } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/User/Templates/Contexts/UserLoginContext.swift ================================================ struct UserLoginContext { let icon: String let title: String let message: String let form: TemplateRepresentable init( icon: String, title: String, message: String, form: TemplateRepresentable ) { self.icon = icon self.title = title self.message = message self.form = form } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/User/Templates/Html/UserLoginTemplate.swift ================================================ import Vapor import SwiftHtml struct UserLoginTemplate: TemplateRepresentable { var context: UserLoginContext init( _ context: UserLoginContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") context.form.render(req) } .id("user-login") .class("container") } .render(req) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/User/UserModule.swift ================================================ import Vapor struct UserModule: ModuleInterface { let router = UserRouter() func boot(_ app: Application) throws { app.migrations.add(UserMigrations.v1()) app.migrations.add(UserMigrations.seed()) app.middleware.use(UserSessionAuthenticator()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/User/UserRouter.swift ================================================ import Vapor struct UserRouter: RouteCollection { let frontendController = UserFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get("sign-in", use: frontendController.signInView) routes .grouped( UserCredentialsAuthenticator() ) .post("sign-in", use: frontendController.signInAction) routes.get("sign-out", use: frontendController.signOut) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/Web/Controllers/WebFrontendController.swift ================================================ import Vapor struct WebFrontendController { func homeView(req: Request) throws -> Response { let ctx = WebHomeContext( icon: "👋", title: "Home", message: "Hi there, welcome to my page.", paragraphs: [ "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.", "Nisi ut aliquip ex ea commodo consequat.", ], link: .init( label: "Read my blog →", url: "/blog/" ) ) return req.templates.renderHtml( WebHomeTemplate(ctx) ) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/Web/Templates/Contexts/WebHomeContext.swift ================================================ struct WebHomeContext { let icon: String let title: String let message: String let paragraphs: [String] let link: WebLinkContext } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/Web/Templates/Contexts/WebIndexContext.swift ================================================ public struct WebIndexContext { public let title: String public init( title: String ) { self.title = title } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/Web/Templates/Contexts/WebLinkContext.swift ================================================ public struct WebLinkContext { public let label: String public let url: String public init( label: String, url: String ) { self.label = label self.url = url } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/Web/Templates/Html/WebHomeTemplate.swift ================================================ import Vapor import SwiftHtml struct WebHomeTemplate: TemplateRepresentable { var context: WebHomeContext init( _ context: WebHomeContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") for paragraph in context.paragraphs { P(paragraph) } WebLinkTemplate(context.link).render(req) } .id("home") .class("container") } .render(req) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg extension Svg { static func menuIcon() -> Svg { Svg { Line(x1: 3, y1: 12, x2: 21, y2: 12) Line(x1: 3, y1: 6, x2: 21, y2: 6) Line(x1: 3, y1: 18, x2: 21, y2: 18) } .width(24) .height(24) .viewBox(minX: 0, minY: 0, width: 24, height: 24) .fill("none") .stroke("currentColor") .strokeWidth(2) .strokeLinecap("round") .strokeLinejoin("round") } } public struct WebIndexTemplate: TemplateRepresentable { public var context: WebIndexContext var body: Tag public init( _ context: WebIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Link(rel: .shortcutIcon) .href("/img/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css") Link(rel: .stylesheet) .href("/css/web.css") Title(context.title) } Body { Header { Div { A { Img(src: "/img/logo.png", alt: "Logo") } .id("site-logo") .href("/") Nav { Input() .type(.checkbox) .id("primary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("primary-menu-button") Div { A("Home") .href("/") .class("selected", req.url.path == "/") A("Blog") .href("/blog/") .class("selected", req.url.path == "/blog/") A("About") .href("#") .onClick("javascript:about();") if req.auth.has(AuthenticatedUser.self) { A("Sign out") .href("/sign-out/") } else { A("Sign in") .href("/sign-in/") } } .class("menu-items") } .id("primary-menu") } .id("navigation") } Main { body } Footer { Section { P { Text("This site is powered by ") A("Swift") .href("https://swift.org") .target(.blank) Text(" & ") A("Vapor") .href("https://vapor.codes") .target(.blank) Text(".") } P("myPage © 2020-2022") } } Script() .type(.javascript) .src("/js/web.js") } } .lang("en-US") } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/Web/Templates/Html/WebLinkTemplate.swift ================================================ import Vapor import SwiftHtml struct WebLinkTemplate: TemplateRepresentable { var context: WebLinkContext init( _ context: WebLinkContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { A(context.label) .href(context.url) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/Web/WebModule.swift ================================================ import Vapor struct WebModule: ModuleInterface { let router = WebRouter() func boot(_ app: Application) throws { try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Modules/Web/WebRouter.swift ================================================ import Vapor struct WebRouter: RouteCollection { let frontendController = WebFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get(use: frontendController.homeView) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Template/Request+Template.swift ================================================ import Vapor public extension Request { var templates: TemplateRenderer { .init(self) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Template/TemplateRenderer.swift ================================================ import Vapor import SwiftHtml public struct TemplateRenderer { var req: Request init(_ req: Request) { self.req = req } public func renderHtml( _ template: TemplateRepresentable, minify: Bool = false, indent: Int = 4 ) -> Response { let doc = Document(.html) { template.render(req) } let body = DocumentRenderer( minify: minify, indent: indent ) .render(doc) return Response( status: .ok, headers: [ "content-type": "text/html" ], body: .init(string: body) ) } } ================================================ FILE: Chapter 07/myProject/Sources/App/Template/TemplateRepresentable.swift ================================================ import Vapor import SwiftSgml public protocol TemplateRepresentable { @TagBuilder func render(_ req: Request) -> Tag } ================================================ FILE: Chapter 07/myProject/Sources/App/configure.swift ================================================ import Vapor import Fluent import FluentSQLiteDriver public func configure( _ app: Application ) throws { let dbPath = app.directory.resourcesDirectory + "db.sqlite" app.databases.use(.sqlite(.file(dbPath)), as: .sqlite) app.middleware.use( FileMiddleware( publicDirectory: app.directory.publicDirectory ) ) app.middleware.use(ExtendPathMiddleware()) app.sessions.use(.fluent) app.migrations.add(SessionRecord.migration) app.middleware.use(app.sessions.middleware) let modules: [ModuleInterface] = [ WebModule(), UserModule(), BlogModule(), ] for module in modules { try module.boot(app) } try app.autoMigrate().wait() } ================================================ FILE: Chapter 07/myProject/Sources/App/routes.swift ================================================ import Vapor import SwiftHtml func routes(_ app: Application) throws { app.routes.get { req -> Response in req.templates.renderHtml( WebIndexTemplate( WebIndexContext( title: "Home" ) ) { P("Hi there, welcome to my page!") } ) } } ================================================ FILE: Chapter 07/myProject/Sources/Run/main.swift ================================================ import App import Vapor var env = try Environment.detect() try LoggingSystem.bootstrap(from: &env) let app = Application(env) defer { app.shutdown() } try configure(app) try app.run() ================================================ FILE: Chapter 07/myProject/Tests/AppTests/AppTests.swift ================================================ @testable import App import XCTVapor final class AppTests: XCTestCase { func testHelloWorld() throws { let app = Application(.testing) defer { app.shutdown() } try configure(app) try app.test(.GET, "hello", afterResponse: { res in XCTAssertEqual(res.status, .ok) XCTAssertEqual(res.body.string, "Hello, world!") }) } } ================================================ FILE: Chapter 07/myProject/docker-compose.yml ================================================ # 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 # Stop all: docker compose down # x-shared_environment: &shared_environment LOG_LEVEL: ${LOG_LEVEL:-debug} services: app: image: myproject:latest build: context: . environment: <<: *shared_environment 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"] ================================================ FILE: Chapter 08/myProject/Dockerfile ================================================ # ================================ # Build image # ================================ FROM swift:6.0-noble 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 \ $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true) # Copy entire repo into container COPY . . # Build the application, 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 \ --product myProject \ --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)/myProject" ./ # 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:noble # 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 ["./myProject"] CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] ================================================ FILE: Chapter 08/myProject/Package.swift ================================================ // swift-tools-version:5.7 import PackageDescription let package = Package( name: "myProject", platforms: [ .macOS(.v12) ], dependencies: [ .package( url: "https://github.com/vapor/vapor", from: "4.70.0" ), .package( url: "https://github.com/vapor/fluent", from: "4.4.0" ), .package( url: "https://github.com/vapor/fluent-sqlite-driver", from: "4.1.0" ), .package( url: "https://github.com/binarybirds/liquid", from: "1.3.0" ), .package( url: "https://github.com/binarybirds/liquid-local-driver", from: "1.3.0" ), .package( url: "https://github.com/binarybirds/swift-html", from: "1.7.0" ), ], targets: [ .target(name: "App", dependencies: [ .product(name: "Vapor", package: "vapor"), .product(name: "Fluent", package: "fluent"), .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), .product(name: "Liquid", package: "liquid"), .product(name: "LiquidLocalDriver", package: "liquid-local-driver"), .product(name: "SwiftHtml", package: "swift-html"), .product(name: "SwiftSvg", package: "swift-html"), ]), .executableTarget(name: "Run", dependencies: ["App"]), .testTarget(name: "AppTests", dependencies: [ .target(name: "App"), .product(name: "XCTVapor", package: "vapor"), ]) ] ) ================================================ FILE: Chapter 08/myProject/Public/css/web.css ================================================ #blog h2 { margin: 0.5rem 0; } ================================================ FILE: Chapter 08/myProject/Public/js/web.js ================================================ function about() { alert("myPage\n\nversion 1.0.0"); } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/AuthenticatedUser.swift ================================================ import Vapor public struct AuthenticatedUser { public let id: UUID public let email: String public init( id: UUID, email: String ) { self.id = id self.email = email } } extension AuthenticatedUser: SessionAuthenticatable { public var sessionID: UUID { id } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/DatabaseModelInterface.swift ================================================ import Vapor import Fluent public protocol DatabaseModelInterface: Fluent.Model where Self.IDValue == UUID { associatedtype Module: ModuleInterface static var identifier: String { get } } public extension DatabaseModelInterface { static var schema: String { Module.identifier + "_" + identifier } static var identifier: String { String(describing: self) .dropFirst(Module.identifier.count) .dropLast(5) .lowercased() + "s" } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Extensions/ByteBuffer+Data.swift ================================================ import Vapor public extension ByteBuffer { var data: Data? { getData(at: 0, length: readableBytes) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Extensions/File+ByteBuffer.swift ================================================ import Vapor public extension File { var byteBuffer: ByteBuffer { data } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/AbstractForm.swift ================================================ import Vapor open class AbstractForm: FormComponent { open var action: FormAction open var fields: [FormComponent] open var error: String? open var submit: String? public init( action: FormAction = .init(), fields: [FormComponent] = [], error: String? = nil, submit: String? = nil ) { self.action = action self.fields = fields self.error = error self.submit = submit self.action.enctype = .multipart } open func load(req: Request) async throws { for field in fields { try await field.load(req: req) } } open func process(req: Request) async throws { for field in fields { try await field.process(req: req) } } open func validate(req: Request) async throws -> Bool { var result: [Bool] = [] for field in fields { result.append(try await field.validate(req: req)) } return result.filter { $0 == false }.isEmpty } open func write(req: Request) async throws { for field in fields { try await field.write(req: req) } } open func save(req: Request) async throws { for field in fields { try await field.save(req: req) } } open func read(req: Request) async throws { for field in fields { try await field.read(req: req) } } open func render(req: Request) -> TemplateRepresentable { FormTemplate( .init( action: action, fields: fields.map { $0.render(req: req)}, error: error, submit: submit ) ) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/AbstractFormField.swift ================================================ import Vapor open class AbstractFormField< Input: Decodable, Output: TemplateRepresentable >: FormComponent { public var key: String public var input: Input public var output: Output public var error: String? // MARK: - event blocks public typealias FormFieldBlock = (Request, AbstractFormField) async throws -> Void public typealias FormFieldValidatorsBlock = ((Request, AbstractFormField) -> [AsyncValidator]) private var readBlock: FormFieldBlock? private var writeBlock: FormFieldBlock? private var loadBlock: FormFieldBlock? private var saveBlock: FormFieldBlock? private var validatorsBlock: FormFieldValidatorsBlock? // MARK: - init & config public init( key: String, input: Input, output: Output, error: String? = nil ) { self.key = key self.input = input self.output = output self.error = error } open func config( _ block: (AbstractFormField) -> Void ) -> Self { block(self) return self } // MARK: - Block setters open func read(_ block: @escaping FormFieldBlock) -> Self { readBlock = block return self } open func write(_ block: @escaping FormFieldBlock) -> Self { writeBlock = block return self } open func load(_ block: @escaping FormFieldBlock) -> Self { loadBlock = block return self } open func save(_ block: @escaping FormFieldBlock) -> Self { saveBlock = block return self } open func validators( @AsyncValidatorBuilder _ block: @escaping FormFieldValidatorsBlock ) -> Self { validatorsBlock = block return self } // MARK: - FormComponent open func process(req: Request) async throws { if let value = try? req.content.get(Input.self, at: key) { input = value } } open func validate(req: Request) async throws -> Bool { guard let validators = validatorsBlock else { return true } return await RequestValidator(validators(req, self)).isValid(req) } open func read(req: Request) async throws { try await readBlock?(req, self) } open func write(req: Request) async throws { try await writeBlock?(req, self) } open func load(req: Request) async throws { try await loadBlock?(req, self) } open func save(req: Request) async throws { try await saveBlock?(req, self) } open func render(req: Request) -> TemplateRepresentable { output } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/Fields/HiddenField.swift ================================================ import Vapor public final class HiddenField: AbstractFormField< String, HiddenFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/Fields/ImageField.swift ================================================ import Vapor public final class ImageField: AbstractFormField< FormImageInput, ImageFieldTemplate > { public var imageKey: String? { didSet { output.context.data.originalKey = imageKey } } public var path: String public init( _ key: String, path: String ) { self.path = path super.init( key: key, input: .init(key: key), output: .init(.init(key: key)) ) } public override func process( req: Request ) async throws { /// process input input.file = try? req.content.get( File.self, at: key ) input.data.originalKey = try? req.content.get( String.self, at: key + "OriginalKey" ) if let temporaryFileKey = try? req.content.get( String.self, at: key + "TemporaryFileKey" ), let temporaryFileName = try? req.content.get( String.self, at: key + "TemporaryFileName" ) { input.data.temporaryFile = .init( key: temporaryFileKey, name: temporaryFileName ) } input.data.shouldRemove = (try? req.content.get( Bool.self, at: key + "ShouldRemove" )) ?? false /// remove & upload file if input.data.shouldRemove { if let originalKey = input.data.originalKey { try? await req.fs.delete(key: originalKey) } } else if let file = input.file, let data = file.byteBuffer.data, !data.isEmpty { if let tmpKey = input.data.temporaryFile?.key { try? await req.fs.delete(key: tmpKey) } let key = "tmp/\(UUID().uuidString).tmp" _ = try await req.fs.upload(key: key, data: data) /// update the temporary image input.data.temporaryFile = .init( key: key, name: file.filename ) } /// update output values output.context.data = input.data } public override func write( req: Request ) async throws { imageKey = input.data.originalKey if input.data.shouldRemove { if let key = input.data.originalKey { try? await req.fs.delete(key: key) } imageKey = nil } else if let file = input.data.temporaryFile { var newKey = path + "/" + file.name if await req.fs.exists(key: newKey) { let formatter = DateFormatter() formatter.dateFormat="y-MM-dd-HH-mm-ss-" let prefix = formatter.string(from: .init()) newKey = path + "/" + prefix + file.name } _ = try await req.fs.move(key: file.key, to: newKey) input.data.temporaryFile = nil if let key = input.data.originalKey { try? await req.fs.delete(key: key) } imageKey = newKey } try await super.write(req: req) } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/Fields/InputField.swift ================================================ public final class InputField: AbstractFormField< String, InputFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init( key: key ) ) ) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/Fields/SelectField.swift ================================================ import Vapor public final class SelectField: AbstractFormField< String, SelectFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/Fields/TextareaField.swift ================================================ import Vapor public final class TextareaField: AbstractFormField< String, TextareaFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/FormAction.swift ================================================ import SwiftHtml public struct FormAction { public var method: SwiftHtml.Method public var url: String? public var enctype: SwiftHtml.Enctype? public init( method: SwiftHtml.Method = .post, url: String? = nil, enctype: SwiftHtml.Enctype? = nil ) { self.method = method self.url = url self.enctype = enctype } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/FormComponent.swift ================================================ import Vapor public protocol FormComponent { func load(req: Request) async throws func process(req: Request) async throws func validate(req: Request) async throws -> Bool func write(req: Request) async throws func save(req: Request) async throws func read(req: Request) async throws func render(req: Request) -> TemplateRepresentable } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/FormComponentBuilder.swift ================================================ @resultBuilder public enum FormComponentBuilder { public static func buildBlock( _ components: FormComponent... ) -> [FormComponent] { components } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/FormImageData.swift ================================================ public struct FormImageData: Codable { public struct TemporaryFile: Codable { public let key: String public let name: String public init( key: String, name: String ) { self.key = key self.name = name } } public var originalKey: String? public var temporaryFile: TemporaryFile? public var shouldRemove: Bool public init( originalKey: String? = nil, temporaryFile: TemporaryFile? = nil, shouldRemove: Bool = false ) { self.originalKey = originalKey self.temporaryFile = temporaryFile self.shouldRemove = shouldRemove } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/FormImageInput.swift ================================================ import Vapor public struct FormImageInput: Codable { public var key: String public var file: File? public var data: FormImageData public init( key: String, file: File? = nil, data: FormImageData? = nil ) { self.key = key self.file = file self.data = data ?? .init() } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/Templates/Contexts/FormContext.swift ================================================ public struct FormContext { public var action: FormAction public var fields: [TemplateRepresentable] public var error: String? public var submit: String? } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/Templates/Contexts/HiddenFieldContext.swift ================================================ public struct HiddenFieldContext { public let key: String public var value: String? public init( key: String, value: String? = nil ) { self.key = key self.value = value } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/Templates/Contexts/ImageFieldContext.swift ================================================ public struct ImageFieldContext { public let key: String public var label: LabelContext public var data: FormImageData public var previewUrl: String? public var accept: String? public var error: String? public init( key: String, label: LabelContext? = nil, data: FormImageData = .init(), previewUrl: String? = nil, accept: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.data = data self.previewUrl = previewUrl self.accept = accept self.error = error } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/Templates/Contexts/InputFieldContext.swift ================================================ import SwiftHtml public struct InputFieldContext { public let key: String public var label: LabelContext public var type: SwiftHtml.Input.`Type` public var placeholder: String? public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, type: Input.`Type` = .text, placeholder: String? = nil, value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.type = type self.placeholder = placeholder self.value = value self.error = error } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/Templates/Contexts/LabelContext.swift ================================================ public struct LabelContext { public var key: String public var title: String? public var required: Bool public var more: String? public init( key: String, title: String? = nil, required: Bool = false, more: String? = nil ) { self.key = key self.title = title self.required = required self.more = more } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/Templates/Contexts/OptionContext.swift ================================================ public struct OptionContext { public var key: String public var label: String public init( key: String, label: String ) { self.key = key self.label = label } } public extension OptionContext { static func yesNo() -> [OptionContext] { ["yes", "no"].map { .init(key: $0, label: $0.capitalized) } } static func trueFalse() -> [OptionContext] { [true, false].map { .init(key: String($0), label: String($0).capitalized) } } static func numbers( _ numbers: [Int] ) -> [OptionContext] { numbers.map { .init(key: String($0), label: String($0)) } } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/Templates/Contexts/SelectFieldContext.swift ================================================ public struct SelectFieldContext { public let key: String public var label: LabelContext public var options: [OptionContext] public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, options: [OptionContext] = [], value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.options = options self.value = value self.error = error } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/Templates/Contexts/TextareaFieldContext.swift ================================================ public struct TextareaFieldContext { public let key: String public var label: LabelContext public var placeholder: String? public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, placeholder: String? = nil, value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.placeholder = placeholder self.value = value self.error = error } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/Templates/Html/FormTemplate.swift ================================================ import Vapor import SwiftHtml public struct FormTemplate: TemplateRepresentable { var context: FormContext public init( _ context: FormContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Form { if let error = context.error { Section { P(error) .class("error") } } for field in context.fields { Section { field.render(req) } } Section { Input() .type(.submit) .value(context.submit ?? "Save") } } .method(context.action.method) .action(context.action.url) .enctype(context.action.enctype) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/Templates/Html/HiddenFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct HiddenFieldTemplate: TemplateRepresentable { var context: HiddenFieldContext public init( _ context: HiddenFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Input() .type(.hidden) .name(context.key) .value(context.value) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/Templates/Html/ImageFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct ImageFieldTemplate: TemplateRepresentable { public var context: ImageFieldContext public init( _ context: ImageFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { if let url = context.previewUrl { Img(src: req.fs.resolve(key: url), alt: context.key) } else { Img(src: "/img/logo.png", alt: "default post image") } LabelTemplate(context.label).render(req) Input() .type(.file) .key(context.key) .class("field") .accept(context.accept) if let temporaryFile = context.data.temporaryFile { Input() .key(context.key + "TemporaryFileKey") .value(temporaryFile.key) .type(.hidden) Input() .key(context.key + "TemporaryFileName") .value(temporaryFile.name) .type(.hidden) } if let key = context.data.originalKey { Input() .key(context.key + "OriginalKey") .value(key) .type(.hidden) } if !context.label.required { Input() .key(context.key + "ShouldRemove") .value(String(true)) .type(.checkbox) .checked(context.data.shouldRemove) Label("Remove") .for(context.key + "Remove") } if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/Templates/Html/InputFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct InputFieldTemplate: TemplateRepresentable { public var context: InputFieldContext public init( _ context: InputFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Input() .type(context.type) .key(context.key) .placeholder(context.placeholder) .value(context.value) .class("field") if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/Templates/Html/LabelTemplate.swift ================================================ import Vapor import SwiftHtml public struct LabelTemplate: TemplateRepresentable { var context: LabelContext public init( _ context: LabelContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Label { Text(context.title ?? context.key.capitalized) if let more = context.more { Span(more) .class("more") } if context.required { Span("*") .class("required") } }.for(context.key) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/Templates/Html/SelectFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct SelectFieldTemplate: TemplateRepresentable { public var context: SelectFieldContext public init( _ context: SelectFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Select { for item in context.options { Option(item.label) .value(item.key) .selected(context.value == item.key) } } .name(context.key) if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Form/Templates/Html/TextareaFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct TextareaFieldTemplate: TemplateRepresentable { public var context: TextareaFieldContext public init( _ context: TextareaFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Textarea(context.value) .placeholder(context.placeholder) .name(context.key) if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/ModuleInterface.swift ================================================ import Vapor public protocol ModuleInterface { static var identifier: String { get } func boot(_ app: Application) throws } public extension ModuleInterface { func boot(_ app: Application) throws {} static var identifier: String { String(describing: self).dropLast(6).lowercased() } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Validation/AsyncValidator.swift ================================================ import Vapor public protocol AsyncValidator { var key: String { get } var message: String { get } func validate( _ req: Request ) async throws -> ValidationErrorDetail? } public extension AsyncValidator { var error: ValidationErrorDetail { .init(key: key, message: message) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Validation/AsyncValidatorBuilder.swift ================================================ @resultBuilder public enum AsyncValidatorBuilder { public static func buildBlock( _ components: AsyncValidator... ) -> [AsyncValidator] { components } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Validation/FormFieldValidator+Validations.swift ================================================ import Vapor public extension FormFieldValidator where Input == String { static func required( _ field: AbstractFormField, _ message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is required" return .init(field, msg) { _, field in !field.input.isEmpty } } static func min( _ field: AbstractFormField, length: Int, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)" return .init(field, msg) { _, field in field.input.count >= length } } static func max( _ field: AbstractFormField, length: Int, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)" return .init(field, msg) { _, field in field.input.count <= length } } static func alphanumeric( _ field: AbstractFormField, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) should be only alphanumeric characters" return .init(field, msg) { _, field in !Validator .characterSet(.alphanumerics) .validate(field.input) .isFailure } } static func email( _ field: AbstractFormField, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) should be a valid email address" return .init(field, msg) { _, field in !Validator .email .validate(field.input) .isFailure } } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Validation/FormFieldValidator.swift ================================================ import Vapor public struct FormFieldValidator< Input: Decodable, Output: TemplateRepresentable >: AsyncValidator { public typealias AsyncValidationBlock = ((Request, AbstractFormField) async throws -> Bool) public let field: AbstractFormField public let message: String public let validation: AsyncValidationBlock public var key: String { field.key } public init( _ field: AbstractFormField, _ message: String, _ validation: @escaping AsyncValidationBlock ) { self.field = field self.message = message self.validation = validation } public func validate( _ req: Request ) async throws -> ValidationErrorDetail? { let isValid = try await validation(req, field) if isValid { return nil } field.error = message return error } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Validation/RequestValidator.swift ================================================ import Vapor public struct RequestValidator { public var validators: [AsyncValidator] public init( _ validators: [AsyncValidator] ) { self.validators = validators } public func validate( _ req: Request, message: String? = nil ) async throws { var result: [ValidationErrorDetail] = [] for validator in validators { if result.contains(where: { $0.key == validator.key }) { continue } if let res = try await validator.validate(req) { result.append(res) } } if !result.isEmpty { throw ValidationAbort( abort: Abort(.badRequest, reason: message), details: result ) } } public func isValid( _ req: Request ) async -> Bool { do { try await validate(req, message: nil) return true } catch { return false } } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Validation/ValidationAbort.swift ================================================ import Vapor public struct ValidationAbort: AbortError { public var abort: Abort public var message: String? public var details: [ValidationErrorDetail] public var reason: String { abort.reason } public var status: HTTPStatus { abort.status } public init( abort: Abort, message: String? = nil, details: [ValidationErrorDetail] ) { self.abort = abort self.message = message self.details = details } } ================================================ FILE: Chapter 08/myProject/Sources/App/Framework/Validation/ValidationErrorDetail.swift ================================================ import Vapor public struct ValidationErrorDetail: Codable { public var key: String public var message: String public init( key: String, message: String ) { self.key = key self.message = message } } extension ValidationErrorDetail: Content {} ================================================ FILE: Chapter 08/myProject/Sources/App/Middlewares/ExtendPathMiddleware.swift ================================================ import Vapor struct ExtendPathMiddleware: AsyncMiddleware { func respond( to req: Request, chainingTo next: AsyncResponder ) async throws -> Response { if !req.url.path.hasSuffix("/") && !req.url.path.contains(".") { return req.redirect( to: req.url.path + "/", redirectType: .permanent ) } return try await next.respond( to: req ) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/Blog/BlogModule.swift ================================================ import Vapor struct BlogModule: ModuleInterface { let router = BlogRouter() func boot(_ app: Application) throws { app.migrations.add(BlogMigrations.v1()) app.migrations.add(BlogMigrations.seed()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/Blog/BlogRouter.swift ================================================ import Vapor struct BlogRouter: RouteCollection { let controller = BlogFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get("blog", use: controller.blogView) routes.get(.anything, use: controller.postView) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift ================================================ import Vapor import Fluent struct BlogFrontendController { func blogView(req: Request) async throws -> Response { let postModels = try await BlogPostModel .query(on: req.db) .sort(\.$date, .descending) .all() let posts = try postModels.map { Blog.Post.List( id: try $0.requireID(), title: $0.title, slug: $0.slug, image: $0.imageKey, excerpt: $0.excerpt, date: $0.date ) } let ctx = BlogPostsContext( icon: "🔥", title: "Blog", message: "Hot news and stories about everything.", posts: posts) return req.templates.renderHtml( BlogPostsTemplate(ctx) ) } func postView(req: Request) async throws -> Response { let slug = req.url.path.trimmingCharacters( in: .init(charactersIn: "/") ) guard let post = try await BlogPostModel .query(on: req.db) .filter(\.$slug == slug) .with(\.$category) .first() else { return req.redirect(to: "/") } let ctx = BlogPostContext( post: Blog.Post.Detail( id: post.id!, title: post.title, slug: post.slug, image: post.imageKey, excerpt: post.excerpt, date: post.date, category: .init( id: post.category.id!, title: post.category.title ), content: post.content ) ) return req.templates.renderHtml( BlogPostTemplate(ctx) ) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/Blog/Database/Migrations/BlogMigrations.swift ================================================ import Foundation import Fluent enum BlogMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema) .id() .field(BlogCategoryModel.FieldKeys.v1.title, .string, .required) .create() try await db.schema(BlogPostModel.schema) .id() .field(BlogPostModel.FieldKeys.v1.title, .string, .required) .field(BlogPostModel.FieldKeys.v1.slug, .string, .required) .field(BlogPostModel.FieldKeys.v1.imageKey, .string, .required) .field(BlogPostModel.FieldKeys.v1.excerpt, .data, .required) .field(BlogPostModel.FieldKeys.v1.date, .datetime, .required) .field(BlogPostModel.FieldKeys.v1.content, .data, .required) .field(BlogPostModel.FieldKeys.v1.categoryId, .uuid) .foreignKey( BlogPostModel.FieldKeys.v1.categoryId, references: BlogCategoryModel.schema, .id, onDelete: DatabaseSchema.ForeignKeyAction.setNull, onUpdate: .cascade ) .unique(on: BlogPostModel.FieldKeys.v1.slug) .create() } func revert(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema).delete() try await db.schema(BlogPostModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let categories = (1...4).map { index in BlogCategoryModel(title: "Sample category #\(index)") } try await categories.create(on: db) try await (1...9).map { index in BlogPostModel( id: nil, title: "Sample post #\(index)", slug: "sample-post-\(index)", imageKey: "/img/posts/\(String(format: "%02d", index + 1)).jpg", excerpt: "Lorem ipsum", date: Date().addingTimeInterval(-Double.random(in: 0...(86400 * 60))), content: "Lorem ipsum dolor sit amet.", categoryId: categories[Int.random(in: 0.. Tag { WebIndexTemplate( .init(title: context.post.title) ) { Div { Section { P(dateFormatter.string(from: context.post.date)) H1(context.post.title) P(context.post.excerpt) } .class(["lead", "container"]) Img(src: context.post.image, alt: context.post.title) Article { Text(context.post.content) } .class("container") } .id("post") } .render(req) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostsTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostsTemplate: TemplateRepresentable { var context: BlogPostsContext init( _ context: BlogPostsContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") Div { for post in context.posts { Article { A { Img(src: post.image, alt: post.title) H2(post.title) P(post.excerpt) } .href("/\(post.slug)/") } } } .class("grid-221") } .id("blog") } .render(req) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/User/Authenticators/UserCredentialsAuthenticator.swift ================================================ import Vapor import Fluent struct UserCredentialsAuthenticator: AsyncCredentialsAuthenticator { struct Credentials: Content { let email: String let password: String } func authenticate( credentials: Credentials, for req: Request ) async throws { guard let user = try await UserAccountModel .query(on: req.db) .filter(\.$email == credentials.email) .first() else { return } do { guard try Bcrypt.verify( credentials.password, created: user.password ) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } catch { // do nothing... } } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/User/Authenticators/UserSessionAuthenticator.swift ================================================ import Vapor import Fluent struct UserSessionAuthenticator: AsyncSessionAuthenticator { typealias User = AuthenticatedUser func authenticate( sessionID: User.SessionID, for req: Request ) async throws { guard let user = try await UserAccountModel .find(sessionID, on: req.db) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/User/Controllers/UserFrontendController.swift ================================================ import Vapor struct UserFrontendController { private func renderSignInView( _ req: Request, _ form: UserLoginForm ) -> Response { let template = UserLoginTemplate( .init( icon: "⬇️", title: "Sign in", message: "Please log in with your existing account", form: form.render(req: req) ) ) return req.templates.renderHtml(template) } func signInView( _ req: Request ) async throws -> Response { renderSignInView(req, .init()) } func signInAction( _ req: Request ) async throws -> Response { /// the user is authenticated, we can store the user data inside the session too if let user = req.auth.get(AuthenticatedUser.self) { req.session.authenticate(user) return req.redirect(to: "/") } let form = UserLoginForm() try await form.process(req: req) let isValid = try await form.validate(req: req) if !isValid { form.error = "Invalid email or password." } return renderSignInView(req, form) } func signOut( _ req: Request ) throws -> Response { req.auth.logout(AuthenticatedUser.self) req.session.unauthenticate(AuthenticatedUser.self) return req.redirect(to: "/") } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/User/Database/Migrations/UserMigrations.swift ================================================ import Vapor import Fluent enum UserMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(UserAccountModel.schema) .id() .field(UserAccountModel.FieldKeys.v1.email, .string, .required) .field(UserAccountModel.FieldKeys.v1.password, .string, .required) .unique(on: UserAccountModel.FieldKeys.v1.email) .create() } func revert(on db: Database) async throws { try await db.schema(UserAccountModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let email = "root@localhost.com" let password = "ChangeMe1" let user = UserAccountModel( email: email, password: try Bcrypt.hash(password) ) try await user.create(on: db) } func revert(on db: Database) async throws { try await UserAccountModel.query(on: db).delete() } } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/User/Database/Models/UserAccountModel.swift ================================================ import Vapor import Fluent final class UserAccountModel: DatabaseModelInterface { typealias Module = UserModule struct FieldKeys { struct v1 { static var email: FieldKey { "email" } static var password: FieldKey { "password" } } } @ID() var id: UUID? @Field(key: FieldKeys.v1.email) var email: String @Field(key: FieldKeys.v1.password) var password: String init() { } init( id: UUID? = nil, email: String, password: String ) { self.id = id self.email = email self.password = password } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/User/Forms/UserLoginForm.swift ================================================ import Vapor final class UserLoginForm: AbstractForm { public convenience init() { self.init( action: .init( method: .post, url: "/sign-in/" ), submit: "Sign in" ) self.fields = createFields() } @FormComponentBuilder func createFields() -> [FormComponent] { InputField("email") .config { $0.output.context.label.required = true $0.output.context.type = .email } .validators { FormFieldValidator.required($1) FormFieldValidator.email($1) } InputField("password") .config { $0.output.context.label.required = true $0.output.context.type = .password } .validators { FormFieldValidator.required($1) } } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/User/Templates/Contexts/UserLoginContext.swift ================================================ struct UserLoginContext { let icon: String let title: String let message: String let form: TemplateRepresentable init( icon: String, title: String, message: String, form: TemplateRepresentable ) { self.icon = icon self.title = title self.message = message self.form = form } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/User/Templates/Html/UserLoginTemplate.swift ================================================ import Vapor import SwiftHtml struct UserLoginTemplate: TemplateRepresentable { var context: UserLoginContext init( _ context: UserLoginContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") context.form.render(req) } .id("user-login") .class("container") } .render(req) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/User/UserModule.swift ================================================ import Vapor struct UserModule: ModuleInterface { let router = UserRouter() func boot(_ app: Application) throws { app.migrations.add(UserMigrations.v1()) app.migrations.add(UserMigrations.seed()) app.middleware.use(UserSessionAuthenticator()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/User/UserRouter.swift ================================================ import Vapor struct UserRouter: RouteCollection { let frontendController = UserFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get("sign-in", use: frontendController.signInView) routes .grouped( UserCredentialsAuthenticator() ) .post("sign-in", use: frontendController.signInAction) routes.get("sign-out", use: frontendController.signOut) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/Web/Controllers/WebFrontendController.swift ================================================ import Vapor struct WebFrontendController { func homeView(req: Request) throws -> Response { let ctx = WebHomeContext( icon: "👋", title: "Home", message: "Hi there, welcome to my page.", paragraphs: [ "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.", "Nisi ut aliquip ex ea commodo consequat.", ], link: .init( label: "Read my blog →", url: "/blog/" ) ) return req.templates.renderHtml( WebHomeTemplate(ctx) ) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/Web/Templates/Contexts/WebHomeContext.swift ================================================ struct WebHomeContext { let icon: String let title: String let message: String let paragraphs: [String] let link: WebLinkContext } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/Web/Templates/Contexts/WebIndexContext.swift ================================================ public struct WebIndexContext { public let title: String public init( title: String ) { self.title = title } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/Web/Templates/Contexts/WebLinkContext.swift ================================================ public struct WebLinkContext { public let label: String public let url: String public init( label: String, url: String ) { self.label = label self.url = url } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/Web/Templates/Html/WebHomeTemplate.swift ================================================ import Vapor import SwiftHtml struct WebHomeTemplate: TemplateRepresentable { var context: WebHomeContext init( _ context: WebHomeContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") for paragraph in context.paragraphs { P(paragraph) } WebLinkTemplate(context.link).render(req) } .id("home") .class("container") } .render(req) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg extension Svg { static func menuIcon() -> Svg { Svg { Line(x1: 3, y1: 12, x2: 21, y2: 12) Line(x1: 3, y1: 6, x2: 21, y2: 6) Line(x1: 3, y1: 18, x2: 21, y2: 18) } .width(24) .height(24) .viewBox(minX: 0, minY: 0, width: 24, height: 24) .fill("none") .stroke("currentColor") .strokeWidth(2) .strokeLinecap("round") .strokeLinejoin("round") } } public struct WebIndexTemplate: TemplateRepresentable { public var context: WebIndexContext var body: Tag public init( _ context: WebIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Link(rel: .shortcutIcon) .href("/img/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css") Link(rel: .stylesheet) .href("/css/web.css") Title(context.title) } Body { Header { Div { A { Img(src: "/img/logo.png", alt: "Logo") } .id("site-logo") .href("/") Nav { Input() .type(.checkbox) .id("primary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("primary-menu-button") Div { A("Home") .href("/") .class("selected", req.url.path == "/") A("Blog") .href("/blog/") .class("selected", req.url.path == "/blog/") A("About") .href("#") .onClick("javascript:about();") if req.auth.has(AuthenticatedUser.self) { A("Sign out") .href("/sign-out/") } else { A("Sign in") .href("/sign-in/") } } .class("menu-items") } .id("primary-menu") } .id("navigation") } Main { body } Footer { Section { P { Text("This site is powered by ") A("Swift") .href("https://swift.org") .target(.blank) Text(" & ") A("Vapor") .href("https://vapor.codes") .target(.blank) Text(".") } P("myPage © 2020-2022") } } Script() .type(.javascript) .src("/js/web.js") } } .lang("en-US") } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/Web/Templates/Html/WebLinkTemplate.swift ================================================ import Vapor import SwiftHtml struct WebLinkTemplate: TemplateRepresentable { var context: WebLinkContext init( _ context: WebLinkContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { A(context.label) .href(context.url) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/Web/WebModule.swift ================================================ import Vapor struct WebModule: ModuleInterface { let router = WebRouter() func boot(_ app: Application) throws { try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Modules/Web/WebRouter.swift ================================================ import Vapor struct WebRouter: RouteCollection { let frontendController = WebFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get(use: frontendController.homeView) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Template/Request+Template.swift ================================================ import Vapor public extension Request { var templates: TemplateRenderer { .init(self) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Template/TemplateRenderer.swift ================================================ import Vapor import SwiftHtml public struct TemplateRenderer { var req: Request init(_ req: Request) { self.req = req } public func renderHtml( _ template: TemplateRepresentable, minify: Bool = false, indent: Int = 4 ) -> Response { let doc = Document(.html) { template.render(req) } let body = DocumentRenderer( minify: minify, indent: indent ) .render(doc) return Response( status: .ok, headers: [ "content-type": "text/html" ], body: .init(string: body) ) } } ================================================ FILE: Chapter 08/myProject/Sources/App/Template/TemplateRepresentable.swift ================================================ import Vapor import SwiftSgml public protocol TemplateRepresentable { @TagBuilder func render(_ req: Request) -> Tag } ================================================ FILE: Chapter 08/myProject/Sources/App/configure.swift ================================================ import Vapor import Fluent import FluentSQLiteDriver import Liquid import LiquidLocalDriver public func configure( _ app: Application ) throws { app.fileStorages.use( .local( publicUrl: "http://localhost:8080", publicPath: app.directory.publicDirectory, workDirectory: "assets" ), as: .local ) app.routes.defaultMaxBodySize = "10mb" let dbPath = app.directory.resourcesDirectory + "db.sqlite" app.databases.use(.sqlite(.file(dbPath)), as: .sqlite) app.middleware.use( FileMiddleware( publicDirectory: app.directory.publicDirectory ) ) app.middleware.use(ExtendPathMiddleware()) app.sessions.use(.fluent) app.migrations.add(SessionRecord.migration) app.middleware.use(app.sessions.middleware) let modules: [ModuleInterface] = [ WebModule(), UserModule(), BlogModule(), ] for module in modules { try module.boot(app) } try app.autoMigrate().wait() } ================================================ FILE: Chapter 08/myProject/Sources/App/routes.swift ================================================ import Vapor import SwiftHtml func routes(_ app: Application) throws { app.routes.get { req -> Response in req.templates.renderHtml( WebIndexTemplate( WebIndexContext( title: "Home" ) ) { P("Hi there, welcome to my page!") } ) } } ================================================ FILE: Chapter 08/myProject/Sources/Run/main.swift ================================================ import App import Vapor var env = try Environment.detect() try LoggingSystem.bootstrap(from: &env) let app = Application(env) defer { app.shutdown() } try configure(app) try app.run() ================================================ FILE: Chapter 08/myProject/Tests/AppTests/AppTests.swift ================================================ @testable import App import XCTVapor final class AppTests: XCTestCase { func testHelloWorld() throws { let app = Application(.testing) defer { app.shutdown() } try configure(app) try app.test(.GET, "hello", afterResponse: { res in XCTAssertEqual(res.status, .ok) XCTAssertEqual(res.body.string, "Hello, world!") }) } } ================================================ FILE: Chapter 08/myProject/docker-compose.yml ================================================ # 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 # Stop all: docker compose down # x-shared_environment: &shared_environment LOG_LEVEL: ${LOG_LEVEL:-debug} services: app: image: myproject:latest build: context: . environment: <<: *shared_environment 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"] ================================================ FILE: Chapter 09/myProject/Dockerfile ================================================ # ================================ # Build image # ================================ FROM swift:6.0-noble 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 \ $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true) # Copy entire repo into container COPY . . # Build the application, 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 \ --product myProject \ --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)/myProject" ./ # 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:noble # 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 ["./myProject"] CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] ================================================ FILE: Chapter 09/myProject/Package.swift ================================================ // swift-tools-version:5.7 import PackageDescription let package = Package( name: "myProject", platforms: [ .macOS(.v12) ], dependencies: [ .package( url: "https://github.com/vapor/vapor", from: "4.70.0" ), .package( url: "https://github.com/vapor/fluent", from: "4.4.0" ), .package( url: "https://github.com/vapor/fluent-sqlite-driver", from: "4.1.0" ), .package( url: "https://github.com/binarybirds/liquid", from: "1.3.0" ), .package( url: "https://github.com/binarybirds/liquid-local-driver", from: "1.3.0" ), .package( url: "https://github.com/binarybirds/swift-html", from: "1.7.0" ), ], targets: [ .target(name: "App", dependencies: [ .product(name: "Vapor", package: "vapor"), .product(name: "Fluent", package: "fluent"), .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), .product(name: "Liquid", package: "liquid"), .product(name: "LiquidLocalDriver", package: "liquid-local-driver"), .product(name: "SwiftHtml", package: "swift-html"), .product(name: "SwiftSvg", package: "swift-html"), ]), .executableTarget(name: "Run", dependencies: ["App"]), .testTarget(name: "AppTests", dependencies: [ .target(name: "App"), .product(name: "XCTVapor", package: "vapor"), ]) ] ) ================================================ FILE: Chapter 09/myProject/Public/css/admin.css ================================================ tr { column-gap: 1rem; } td img { width: 100px; } ================================================ FILE: Chapter 09/myProject/Public/css/web.css ================================================ #blog h2 { margin: 0.5rem 0; } ================================================ FILE: Chapter 09/myProject/Public/js/admin.js ================================================ ================================================ FILE: Chapter 09/myProject/Public/js/web.js ================================================ function about() { alert("myPage\n\nversion 1.0.0"); } ================================================ FILE: Chapter 09/myProject/Sources/App/Extensions/Svg+MenuIcon.swift ================================================ import SwiftSvg extension Svg { static func menuIcon() -> Svg { Svg { Line(x1: 3, y1: 12, x2: 21, y2: 12) Line(x1: 3, y1: 6, x2: 21, y2: 6) Line(x1: 3, y1: 18, x2: 21, y2: 18) } .width(24) .height(24) .viewBox(minX: 0, minY: 0, width: 24, height: 24) .fill("none") .stroke("currentColor") .strokeWidth(2) .strokeLinecap("round") .strokeLinejoin("round") } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/AuthenticatedUser.swift ================================================ import Vapor public struct AuthenticatedUser { public let id: UUID public let email: String public init( id: UUID, email: String ) { self.id = id self.email = email } } extension AuthenticatedUser: SessionAuthenticatable { public var sessionID: UUID { id } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/DatabaseModelInterface.swift ================================================ import Vapor import Fluent public protocol DatabaseModelInterface: Fluent.Model where Self.IDValue == UUID { associatedtype Module: ModuleInterface static var identifier: String { get } } public extension DatabaseModelInterface { static var schema: String { Module.identifier + "_" + identifier } static var identifier: String { String(describing: self) .dropFirst(Module.identifier.count) .dropLast(5) .lowercased() + "s" } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Extensions/ByteBuffer+Data.swift ================================================ import Vapor public extension ByteBuffer { var data: Data? { getData(at: 0, length: readableBytes) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Extensions/File+ByteBuffer.swift ================================================ import Vapor public extension File { var byteBuffer: ByteBuffer { data } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/AbstractForm.swift ================================================ import Vapor open class AbstractForm: FormComponent { open var action: FormAction open var fields: [FormComponent] open var error: String? open var submit: String? public init( action: FormAction = .init(), fields: [FormComponent] = [], error: String? = nil, submit: String? = nil ) { self.action = action self.fields = fields self.error = error self.submit = submit self.action.enctype = .multipart } open func load(req: Request) async throws { for field in fields { try await field.load(req: req) } } open func process(req: Request) async throws { for field in fields { try await field.process(req: req) } } open func validate(req: Request) async throws -> Bool { var result: [Bool] = [] for field in fields { result.append(try await field.validate(req: req)) } return result.filter { $0 == false }.isEmpty } open func write(req: Request) async throws { for field in fields { try await field.write(req: req) } } open func save(req: Request) async throws { for field in fields { try await field.save(req: req) } } open func read(req: Request) async throws { for field in fields { try await field.read(req: req) } } open func render(req: Request) -> TemplateRepresentable { FormTemplate( .init( action: action, fields: fields.map { $0.render(req: req)}, error: error, submit: submit ) ) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/AbstractFormField.swift ================================================ import Vapor open class AbstractFormField< Input: Decodable, Output: TemplateRepresentable >: FormComponent { public var key: String public var input: Input public var output: Output public var error: String? // MARK: - event blocks public typealias FormFieldBlock = (Request, AbstractFormField) async throws -> Void public typealias FormFieldValidatorsBlock = ((Request, AbstractFormField) -> [AsyncValidator]) private var readBlock: FormFieldBlock? private var writeBlock: FormFieldBlock? private var loadBlock: FormFieldBlock? private var saveBlock: FormFieldBlock? private var validatorsBlock: FormFieldValidatorsBlock? // MARK: - init & config public init( key: String, input: Input, output: Output, error: String? = nil ) { self.key = key self.input = input self.output = output self.error = error } open func config( _ block: (AbstractFormField) -> Void ) -> Self { block(self) return self } // MARK: - Block setters open func read(_ block: @escaping FormFieldBlock) -> Self { readBlock = block return self } open func write(_ block: @escaping FormFieldBlock) -> Self { writeBlock = block return self } open func load(_ block: @escaping FormFieldBlock) -> Self { loadBlock = block return self } open func save(_ block: @escaping FormFieldBlock) -> Self { saveBlock = block return self } open func validators( @AsyncValidatorBuilder _ block: @escaping FormFieldValidatorsBlock ) -> Self { validatorsBlock = block return self } // MARK: - FormComponent open func process(req: Request) async throws { if let value = try? req.content.get(Input.self, at: key) { input = value } } open func validate(req: Request) async throws -> Bool { guard let validators = validatorsBlock else { return true } return await RequestValidator(validators(req, self)).isValid(req) } open func read(req: Request) async throws { try await readBlock?(req, self) } open func write(req: Request) async throws { try await writeBlock?(req, self) } open func load(req: Request) async throws { try await loadBlock?(req, self) } open func save(req: Request) async throws { try await saveBlock?(req, self) } open func render(req: Request) -> TemplateRepresentable { output } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/Fields/HiddenField.swift ================================================ import Vapor public final class HiddenField: AbstractFormField< String, HiddenFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/Fields/ImageField.swift ================================================ import Vapor public final class ImageField: AbstractFormField< FormImageInput, ImageFieldTemplate > { public var imageKey: String? { didSet { output.context.data.originalKey = imageKey } } public var path: String public init( _ key: String, path: String ) { self.path = path super.init( key: key, input: .init(key: key), output: .init(.init(key: key)) ) } public override func process( req: Request ) async throws { /// process input input.file = try? req.content.get( File.self, at: key ) input.data.originalKey = try? req.content.get( String.self, at: key + "OriginalKey" ) if let temporaryFileKey = try? req.content.get( String.self, at: key + "TemporaryFileKey" ), let temporaryFileName = try? req.content.get( String.self, at: key + "TemporaryFileName" ) { input.data.temporaryFile = .init( key: temporaryFileKey, name: temporaryFileName ) } input.data.shouldRemove = (try? req.content.get( Bool.self, at: key + "ShouldRemove" )) ?? false /// remove & upload file if input.data.shouldRemove { if let originalKey = input.data.originalKey { try? await req.fs.delete(key: originalKey) } } else if let file = input.file, let data = file.byteBuffer.data, !data.isEmpty { if let tmpKey = input.data.temporaryFile?.key { try? await req.fs.delete(key: tmpKey) } let key = "tmp/\(UUID().uuidString).tmp" _ = try await req.fs.upload(key: key, data: data) /// update the temporary image input.data.temporaryFile = .init( key: key, name: file.filename ) } /// update output values output.context.data = input.data } public override func write( req: Request ) async throws { imageKey = input.data.originalKey if input.data.shouldRemove { if let key = input.data.originalKey { try? await req.fs.delete(key: key) } imageKey = nil } else if let file = input.data.temporaryFile { var newKey = path + "/" + file.name if await req.fs.exists(key: newKey) { let formatter = DateFormatter() formatter.dateFormat="y-MM-dd-HH-mm-ss-" let prefix = formatter.string(from: .init()) newKey = path + "/" + prefix + file.name } _ = try await req.fs.move(key: file.key, to: newKey) input.data.temporaryFile = nil if let key = input.data.originalKey { try? await req.fs.delete(key: key) } imageKey = newKey } try await super.write(req: req) } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/Fields/InputField.swift ================================================ public final class InputField: AbstractFormField< String, InputFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init( key: key ) ) ) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/Fields/SelectField.swift ================================================ import Vapor public final class SelectField: AbstractFormField< String, SelectFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/Fields/TextareaField.swift ================================================ import Vapor public final class TextareaField: AbstractFormField< String, TextareaFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/FormAction.swift ================================================ import SwiftHtml public struct FormAction { public var method: SwiftHtml.Method public var url: String? public var enctype: SwiftHtml.Enctype? public init( method: SwiftHtml.Method = .post, url: String? = nil, enctype: SwiftHtml.Enctype? = nil ) { self.method = method self.url = url self.enctype = enctype } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/FormComponent.swift ================================================ import Vapor public protocol FormComponent { func load(req: Request) async throws func process(req: Request) async throws func validate(req: Request) async throws -> Bool func write(req: Request) async throws func save(req: Request) async throws func read(req: Request) async throws func render(req: Request) -> TemplateRepresentable } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/FormComponentBuilder.swift ================================================ @resultBuilder public enum FormComponentBuilder { public static func buildBlock( _ components: FormComponent... ) -> [FormComponent] { components } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/FormImageData.swift ================================================ public struct FormImageData: Codable { public struct TemporaryFile: Codable { public let key: String public let name: String public init( key: String, name: String ) { self.key = key self.name = name } } public var originalKey: String? public var temporaryFile: TemporaryFile? public var shouldRemove: Bool public init( originalKey: String? = nil, temporaryFile: TemporaryFile? = nil, shouldRemove: Bool = false ) { self.originalKey = originalKey self.temporaryFile = temporaryFile self.shouldRemove = shouldRemove } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/FormImageInput.swift ================================================ import Vapor public struct FormImageInput: Codable { public var key: String public var file: File? public var data: FormImageData public init( key: String, file: File? = nil, data: FormImageData? = nil ) { self.key = key self.file = file self.data = data ?? .init() } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/Templates/Contexts/FormContext.swift ================================================ public struct FormContext { public var action: FormAction public var fields: [TemplateRepresentable] public var error: String? public var submit: String? } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/Templates/Contexts/HiddenFieldContext.swift ================================================ public struct HiddenFieldContext { public let key: String public var value: String? public init( key: String, value: String? = nil ) { self.key = key self.value = value } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/Templates/Contexts/ImageFieldContext.swift ================================================ public struct ImageFieldContext { public let key: String public var label: LabelContext public var data: FormImageData public var previewUrl: String? public var accept: String? public var error: String? public init( key: String, label: LabelContext? = nil, data: FormImageData = .init(), previewUrl: String? = nil, accept: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.data = data self.previewUrl = previewUrl self.accept = accept self.error = error } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/Templates/Contexts/InputFieldContext.swift ================================================ import SwiftHtml public struct InputFieldContext { public let key: String public var label: LabelContext public var type: SwiftHtml.Input.`Type` public var placeholder: String? public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, type: Input.`Type` = .text, placeholder: String? = nil, value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.type = type self.placeholder = placeholder self.value = value self.error = error } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/Templates/Contexts/LabelContext.swift ================================================ public struct LabelContext { public var key: String public var title: String? public var required: Bool public var more: String? public init( key: String, title: String? = nil, required: Bool = false, more: String? = nil ) { self.key = key self.title = title self.required = required self.more = more } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/Templates/Contexts/OptionContext.swift ================================================ public struct OptionContext { public var key: String public var label: String public init( key: String, label: String ) { self.key = key self.label = label } } public extension OptionContext { static func yesNo() -> [OptionContext] { ["yes", "no"].map { .init(key: $0, label: $0.capitalized) } } static func trueFalse() -> [OptionContext] { [true, false].map { .init(key: String($0), label: String($0).capitalized) } } static func numbers( _ numbers: [Int] ) -> [OptionContext] { numbers.map { .init(key: String($0), label: String($0)) } } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/Templates/Contexts/SelectFieldContext.swift ================================================ public struct SelectFieldContext { public let key: String public var label: LabelContext public var options: [OptionContext] public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, options: [OptionContext] = [], value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.options = options self.value = value self.error = error } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/Templates/Contexts/TextareaFieldContext.swift ================================================ public struct TextareaFieldContext { public let key: String public var label: LabelContext public var placeholder: String? public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, placeholder: String? = nil, value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.placeholder = placeholder self.value = value self.error = error } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/Templates/Html/FormTemplate.swift ================================================ import Vapor import SwiftHtml public struct FormTemplate: TemplateRepresentable { var context: FormContext public init( _ context: FormContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Form { if let error = context.error { Section { P(error) .class("error") } } for field in context.fields { Section { field.render(req) } } Section { Input() .type(.submit) .value(context.submit ?? "Save") } } .method(context.action.method) .action(context.action.url) .enctype(context.action.enctype) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/Templates/Html/HiddenFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct HiddenFieldTemplate: TemplateRepresentable { var context: HiddenFieldContext public init( _ context: HiddenFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Input() .type(.hidden) .name(context.key) .value(context.value) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/Templates/Html/ImageFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct ImageFieldTemplate: TemplateRepresentable { public var context: ImageFieldContext public init( _ context: ImageFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { if let url = context.previewUrl { Img(src: req.fs.resolve(key: url), alt: context.key) } else { Img(src: "/img/logo.png", alt: "default post image") } LabelTemplate(context.label).render(req) Input() .type(.file) .key(context.key) .class("field") .accept(context.accept) if let temporaryFile = context.data.temporaryFile { Input() .key(context.key + "TemporaryFileKey") .value(temporaryFile.key) .type(.hidden) Input() .key(context.key + "TemporaryFileName") .value(temporaryFile.name) .type(.hidden) } if let key = context.data.originalKey { Input() .key(context.key + "OriginalKey") .value(key) .type(.hidden) } if !context.label.required { Input() .key(context.key + "ShouldRemove") .value(String(true)) .type(.checkbox) .checked(context.data.shouldRemove) Label("Remove") .for(context.key + "Remove") } if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/Templates/Html/InputFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct InputFieldTemplate: TemplateRepresentable { public var context: InputFieldContext public init( _ context: InputFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Input() .type(context.type) .key(context.key) .placeholder(context.placeholder) .value(context.value) .class("field") if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/Templates/Html/LabelTemplate.swift ================================================ import Vapor import SwiftHtml public struct LabelTemplate: TemplateRepresentable { var context: LabelContext public init( _ context: LabelContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Label { Text(context.title ?? context.key.capitalized) if let more = context.more { Span(more) .class("more") } if context.required { Span("*") .class("required") } }.for(context.key) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/Templates/Html/SelectFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct SelectFieldTemplate: TemplateRepresentable { public var context: SelectFieldContext public init( _ context: SelectFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Select { for item in context.options { Option(item.label) .value(item.key) .selected(context.value == item.key) } } .name(context.key) if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Form/Templates/Html/TextareaFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct TextareaFieldTemplate: TemplateRepresentable { public var context: TextareaFieldContext public init( _ context: TextareaFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Textarea(context.value) .placeholder(context.placeholder) .name(context.key) if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/ModuleInterface.swift ================================================ import Vapor public protocol ModuleInterface { static var identifier: String { get } func boot(_ app: Application) throws } public extension ModuleInterface { func boot(_ app: Application) throws {} static var identifier: String { String(describing: self).dropLast(6).lowercased() } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Validation/AsyncValidator.swift ================================================ import Vapor public protocol AsyncValidator { var key: String { get } var message: String { get } func validate( _ req: Request ) async throws -> ValidationErrorDetail? } public extension AsyncValidator { var error: ValidationErrorDetail { .init(key: key, message: message) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Validation/AsyncValidatorBuilder.swift ================================================ @resultBuilder public enum AsyncValidatorBuilder { public static func buildBlock( _ components: AsyncValidator... ) -> [AsyncValidator] { components } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Validation/FormFieldValidator+Validations.swift ================================================ import Vapor public extension FormFieldValidator where Input == String { static func required( _ field: AbstractFormField, _ message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is required" return .init(field, msg) { _, field in !field.input.isEmpty } } static func min( _ field: AbstractFormField, length: Int, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)" return .init(field, msg) { _, field in field.input.count >= length } } static func max( _ field: AbstractFormField, length: Int, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)" return .init(field, msg) { _, field in field.input.count <= length } } static func alphanumeric( _ field: AbstractFormField, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) should be only alphanumeric characters" return .init(field, msg) { _, field in !Validator .characterSet(.alphanumerics) .validate(field.input) .isFailure } } static func email( _ field: AbstractFormField, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) should be a valid email address" return .init(field, msg) { _, field in !Validator .email .validate(field.input) .isFailure } } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Validation/FormFieldValidator.swift ================================================ import Vapor public struct FormFieldValidator< Input: Decodable, Output: TemplateRepresentable >: AsyncValidator { public typealias AsyncValidationBlock = ((Request, AbstractFormField) async throws -> Bool) public let field: AbstractFormField public let message: String public let validation: AsyncValidationBlock public var key: String { field.key } public init( _ field: AbstractFormField, _ message: String, _ validation: @escaping AsyncValidationBlock ) { self.field = field self.message = message self.validation = validation } public func validate( _ req: Request ) async throws -> ValidationErrorDetail? { let isValid = try await validation(req, field) if isValid { return nil } field.error = message return error } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Validation/RequestValidator.swift ================================================ import Vapor public struct RequestValidator { public var validators: [AsyncValidator] public init( _ validators: [AsyncValidator] ) { self.validators = validators } public func validate( _ req: Request, message: String? = nil ) async throws { var result: [ValidationErrorDetail] = [] for validator in validators { if result.contains(where: { $0.key == validator.key }) { continue } if let res = try await validator.validate(req) { result.append(res) } } if !result.isEmpty { throw ValidationAbort( abort: Abort(.badRequest, reason: message), details: result ) } } public func isValid( _ req: Request ) async -> Bool { do { try await validate(req, message: nil) return true } catch { return false } } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Validation/ValidationAbort.swift ================================================ import Vapor public struct ValidationAbort: AbortError { public var abort: Abort public var message: String? public var details: [ValidationErrorDetail] public var reason: String { abort.reason } public var status: HTTPStatus { abort.status } public init( abort: Abort, message: String? = nil, details: [ValidationErrorDetail] ) { self.abort = abort self.message = message self.details = details } } ================================================ FILE: Chapter 09/myProject/Sources/App/Framework/Validation/ValidationErrorDetail.swift ================================================ import Vapor public struct ValidationErrorDetail: Codable { public var key: String public var message: String public init( key: String, message: String ) { self.key = key self.message = message } } extension ValidationErrorDetail: Content {} ================================================ FILE: Chapter 09/myProject/Sources/App/Middlewares/ExtendPathMiddleware.swift ================================================ import Vapor struct ExtendPathMiddleware: AsyncMiddleware { func respond( to req: Request, chainingTo next: AsyncResponder ) async throws -> Response { if !req.url.path.hasSuffix("/") && !req.url.path.contains(".") { return req.redirect( to: req.url.path + "/", redirectType: .permanent ) } return try await next.respond( to: req ) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Admin/AdminModule.swift ================================================ import Vapor struct AdminModule: ModuleInterface { let router = AdminRouter() func boot(_ app: Application) throws { try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Admin/AdminRouter.swift ================================================ import Vapor struct AdminRouter: RouteCollection { let controller = AdminFrontendController() func boot(routes: RoutesBuilder) throws { routes .grouped( AuthenticatedUser.redirectMiddleware( path: "/sign-in/" ) ) .get("admin", use: controller.dashboardView) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Admin/Controllers/AdminFrontendController.swift ================================================ import Vapor struct AdminFrontendController { func dashboardView( req: Request ) throws -> Response { let user = try req.auth.require(AuthenticatedUser.self) let template = AdminDashboardTemplate( .init( icon: "👋", title: "Dashboard", message: "Hello \(user.email), welcome to the CMS." ) ) return req.templates.renderHtml(template) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDashboardContext.swift ================================================ struct AdminDashboardContext { let icon: String let title: String let message: String } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminIndexContext.swift ================================================ public struct AdminIndexContext { public let title: String public init( title: String ) { self.title = title } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDashboardTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminDashboardTemplate: TemplateRepresentable { var context: AdminDashboardContext init( _ context: AdminDashboardContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } Nav { H2("Blog") Ul { Li { A("Posts") .href("/admin/blog/posts/") } } } } .id("dashboard") .class("container") } .render(req) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Admin/Templates/Html/AdminIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg public struct AdminIndexTemplate: TemplateRepresentable { public var context: AdminIndexContext var body: Tag public init( _ context: AdminIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Meta() .name("robots") .content("noindex") Link(rel: .shortcutIcon) .href("/images/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css") Link(rel: .stylesheet) .href("/css/admin.css") Title(context.title) } Body { Div { A { Img(src: "/img/logo.png", alt: "Logo") .title("Logo") .style("width: 300px") } .href("/") Nav { Input() .type(.checkbox) .id("secondary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("secondary-menu-button") Div { A("Sign out") .href("/sign-out/") } .class("menu-items") } .id("secondary-menu") } .id("navigation") Main { body } Script() .type(.javascript) .src("/js/admin.js") } } .lang("en-US") } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Blog/BlogModule.swift ================================================ import Vapor struct BlogModule: ModuleInterface { let router = BlogRouter() func boot(_ app: Application) throws { app.migrations.add(BlogMigrations.v1()) app.migrations.add(BlogMigrations.seed()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Blog/BlogRouter.swift ================================================ import Vapor struct BlogRouter: RouteCollection { let controller = BlogFrontendController() let postAdminController = BlogPostAdminController() func boot(routes: RoutesBuilder) throws { routes.get("blog", use: controller.blogView) routes.get(.anything, use: controller.postView) let posts = routes .grouped(AuthenticatedUser.redirectMiddleware(path: "/")) .grouped("admin", "blog", "posts") posts.get(use: postAdminController.listView) let postId = posts.grouped(":postId") postId.get(use: postAdminController.detailView) posts.get("create", use: postAdminController.createView) posts.post("create", use: postAdminController.createAction) postId.get("update", use: postAdminController.updateView) postId.post("update", use: postAdminController.updateAction) postId.get("delete", use: postAdminController.deleteView) postId.post("delete", use: postAdminController.deleteAction) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift ================================================ import Vapor import Fluent struct BlogFrontendController { func blogView(req: Request) async throws -> Response { let posts = try await BlogPostModel .query(on: req.db) .sort(\.$date, .descending) .all() let api = BlogPostApiController() let list = posts.map { api.mapList($0) } let ctx = BlogPostsContext( icon: "🔥", title: "Blog", message: "Hot news and stories about everything.", posts: list ) return req.templates.renderHtml(BlogPostsTemplate(ctx)) } func postView(req: Request) async throws -> Response { let slug = req.url.path.trimmingCharacters( in: .init(charactersIn: "/") ) guard let post = try await BlogPostModel .query(on: req.db) .filter(\.$slug == slug) .with(\.$category) .first() else { return req.redirect(to: "/") } let api = BlogPostApiController() let ctx = BlogPostContext(post: api.mapDetail(post)) return req.templates.renderHtml(BlogPostTemplate(ctx)) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Blog/Controllers/BlogPostAdminController.swift ================================================ import Vapor import Fluent struct BlogPostAdminController { func find(_ req: Request) async throws -> BlogPostModel { guard let id = req.parameters.get("postId"), let uuid = UUID(uuidString: id), let post = try await BlogPostModel .query(on: req.db) .filter(\.$id == uuid) .with(\.$category) .first() else { throw Abort(.notFound) } return post } func listView(_ req: Request) async throws -> Response { let posts = try await BlogPostModel.query(on: req.db).all() let api = BlogPostApiController() let list = posts.map { api.mapList($0) } let template = BlogPostAdminListTemplate( .init( title: "Posts", list: list ) ) return req.templates.renderHtml(template) } func detailView(_ req: Request) async throws -> Response { let post = try await find(req) let detail = BlogPostApiController().mapDetail(post) let template = BlogPostAdminDetailTemplate( .init( title: "Post details", detail: detail ) ) return req.templates.renderHtml(template) } private func renderEditForm( _ req: Request, _ title: String, _ form: BlogPostEditForm ) -> Response { let template = BlogPostAdminEditTemplate( .init( title: title, form: form.render(req: req) ) ) return req.templates.renderHtml(template) } func createView( _ req: Request ) async throws -> Response { let model = BlogPostModel() let form = BlogPostEditForm(model) try await form.load(req: req) return renderEditForm(req, "Create post", form) } func createAction( _ req: Request ) async throws -> Response { let model = BlogPostModel() let form = BlogPostEditForm(model) try await form.load(req: req) try await form.process(req: req) let isValid = try await form.validate(req: req) guard isValid else { return renderEditForm(req, "Create post", form) } try await form.write(req: req) try await model.create(on: req.db) try await form.save(req: req) return req.redirect( to: "/admin/blog/posts/\(model.id!.uuidString)/" ) } func updateView(_ req: Request) async throws -> Response { let model = try await find(req) let form = BlogPostEditForm(model) try await form.load(req: req) try await form.read(req: req) return renderEditForm(req, "Update post", form) } func updateAction(_ req: Request) async throws -> Response { let model = try await find(req) let form = BlogPostEditForm(model) try await form.load(req: req) try await form.process(req: req) let isValid = try await form.validate(req: req) guard isValid else { return renderEditForm(req, "Update post", form) } try await form.write(req: req) try await model.update(on: req.db) try await form.save(req: req) return req.redirect( to: "/admin/blog/posts/\(model.id!.uuidString)/update/" ) } func deleteView(_ req: Request) async throws -> Response { let model = try await find(req) let template = BlogPostAdminDeleteTemplate( .init( title: "Delete post", name: model.title, type: "post" ) ) return req.templates.renderHtml(template) } func deleteAction(_ req: Request) async throws -> Response { let model = try await find(req) try await req.fs.delete(key: model.imageKey) try await model.delete(on: req.db) return req.redirect(to: "/admin/blog/posts/") } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Blog/Controllers/BlogPostApiController.swift ================================================ import Vapor struct BlogPostApiController { func mapList(_ model: BlogPostModel) -> Blog.Post.List { .init( id: model.id!, title: model.title, slug: model.slug, image: model.imageKey, excerpt: model.excerpt, date: model.date ) } func mapDetail(_ model: BlogPostModel) -> Blog.Post.Detail { .init( id: model.id!, title: model.title, slug: model.slug, image: model.imageKey, excerpt: model.excerpt, date: model.date, category: .init( id: model.category.id!, title: model.category.title ), content: model.content ) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Blog/Database/Migrations/BlogMigrations.swift ================================================ import Foundation import Fluent enum BlogMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema) .id() .field(BlogCategoryModel.FieldKeys.v1.title, .string, .required) .create() try await db.schema(BlogPostModel.schema) .id() .field(BlogPostModel.FieldKeys.v1.title, .string, .required) .field(BlogPostModel.FieldKeys.v1.slug, .string, .required) .field(BlogPostModel.FieldKeys.v1.imageKey, .string, .required) .field(BlogPostModel.FieldKeys.v1.excerpt, .data, .required) .field(BlogPostModel.FieldKeys.v1.date, .datetime, .required) .field(BlogPostModel.FieldKeys.v1.content, .data, .required) .field(BlogPostModel.FieldKeys.v1.categoryId, .uuid) .foreignKey( BlogPostModel.FieldKeys.v1.categoryId, references: BlogCategoryModel.schema, .id, onDelete: DatabaseSchema.ForeignKeyAction.setNull, onUpdate: .cascade ) .unique(on: BlogPostModel.FieldKeys.v1.slug) .create() } func revert(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema).delete() try await db.schema(BlogPostModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let categories = (1...4).map { index in BlogCategoryModel(title: "Sample category #\(index)") } try await categories.create(on: db) try await (1...9).map { index in BlogPostModel( id: nil, title: "Sample post #\(index)", slug: "sample-post-\(index)", imageKey: "/img/posts/\(String(format: "%02d", index + 1)).jpg", excerpt: "Lorem ipsum", date: Date().addingTimeInterval(-Double.random(in: 0...(86400 * 60))), content: "Lorem ipsum dolor sit amet.", categoryId: categories[Int.random(in: 0.. [FormComponent] { ImageField("image", path: "blog/post") .read { [unowned self] in $1.output.context.previewUrl = model.imageKey ($1 as! ImageField).imageKey = model.imageKey } .write { [unowned self] in model.imageKey = ($1 as! ImageField).imageKey ?? "" } InputField("slug") .config { $0.output.context.label.required = true } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = model.slug } .write { [unowned self] in model.slug = $1.input } InputField("title") .config { $0.output.context.label.required = true } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = model.title } .write { [unowned self] in model.title = $1.input } InputField("date") .config { $0.output.context.label.required = true $0.output.context.value = dateFormatter.string(from: Date()) } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = dateFormatter.string(from: model.date) } .write { [unowned self] in model.date = dateFormatter.date(from: $1.input) ?? Date() } TextareaField("excerpt") .read { [unowned self] in $1.output.context.value = model.excerpt } .write { [unowned self] in model.excerpt = $1.input } TextareaField("content") .read { [unowned self] in $1.output.context.value = model.content } .write { [unowned self] in model.content = $1.input } SelectField("category") .load { req, field in let categories = try await BlogCategoryModel .query(on: req.db) .all() field.output.context.options = categories.map { OptionContext(key: $0.id!.uuidString, label: $0.title) } } .read { [unowned self] req, field in field.output.context.value = model.$category.id.uuidString } .write { [unowned self] req, field in if let uuid = UUID(uuidString: field.input), let category = try await BlogCategoryModel .find(uuid, on: req.db) { model.$category.id = category.id! } } } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Blog/Objects/Blog.swift ================================================ enum Blog { enum Post { } enum Category { } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Blog/Objects/BlogCategory.swift ================================================ import Foundation extension Blog.Category { struct List: Codable { let id: UUID let title: String } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Blog/Objects/BlogPost.swift ================================================ import Foundation extension Blog.Post { struct List: Codable { let id: UUID let title: String let slug: String let image: String let excerpt: String let date: Date } struct Detail: Codable { let id: UUID let title: String let slug: String let image: String let excerpt: String let date: Date let category: Blog.Category.List let content: String } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminDeleteContext.swift ================================================ struct BlogPostAdminDeleteContext { let title: String let name: String let type: String } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminDetailContext.swift ================================================ struct BlogPostAdminDetailContext { let title: String let detail: Blog.Post.Detail } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminEditContext.swift ================================================ struct BlogPostAdminEditContext { let title: String let form: TemplateRepresentable } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminListContext.swift ================================================ struct BlogPostAdminListContext { let title: String let list: [Blog.Post.List] } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostContext.swift ================================================ struct BlogPostContext { let post: Blog.Post.Detail } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostsContext.swift ================================================ struct BlogPostsContext { let icon: String let title: String let message: String let posts: [Blog.Post.List] } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminDeleteTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminDeleteTemplate: TemplateRepresentable { var context: BlogPostAdminDeleteContext init( _ context: BlogPostAdminDeleteContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Span("🗑") .class("icon") H1(context.title) P("You are about to permanently delete the
`\(context.name)` \(context.type).") Form { Input() .type(.submit) .class(["button", "destructive"]) .style("display: inline") .value("Delete") A("Cancel") .href("/admin/blog/posts/") .class(["button", "cancel"]) } .method(.post) .id("delete-form") } .class(["lead", "container", "center"]) } .render(req) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminDetailTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminDetailTemplate: TemplateRepresentable { var context: BlogPostAdminDetailContext init( _ context: BlogPostAdminDetailContext ) { self.context = context } var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .short return formatter }() @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") Dl { Dt("Image") Dd { Img( src: context.detail.image, alt: context.detail.title ) } Dt("Title") Dd(context.detail.title) Dt("Excerpt") Dd(context.detail.excerpt) Dt("Date") Dd(dateFormatter.string(from: context.detail.date)) Dt("Content") Dd(context.detail.content) } } .id("detail") .class("container") } .render(req) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminEditTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminEditTemplate: TemplateRepresentable { var context: BlogPostAdminEditContext init( _ context: BlogPostAdminEditContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") context.form.render(req) } .id("edit") .class("container") } .render(req) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminListTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminListTemplate: TemplateRepresentable { var context: BlogPostAdminListContext init( _ context: BlogPostAdminListContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") Table { Thead { Tr { Th("Image") Th("Title") Th("Preview") } } Tbody { for item in context.list { Tr { Td { Img(src: item.image, alt: item.title) } Td { A(item.title) .href("/admin/blog/posts/" + item.id.uuidString + "/") } Td { A("Preview") .href("/" + item.slug + "/") } } } } } } .id("list") } .render(req) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostTemplate: TemplateRepresentable { var context: BlogPostContext init( _ context: BlogPostContext ) { self.context = context } var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .short return formatter }() @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.post.title) ) { Div { Section { P(dateFormatter.string(from: context.post.date)) H1(context.post.title) P(context.post.excerpt) } .class(["lead", "container"]) Img(src: context.post.image, alt: context.post.title) Article { Text(context.post.content) } .class("container") } .id("post") } .render(req) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostsTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostsTemplate: TemplateRepresentable { var context: BlogPostsContext init( _ context: BlogPostsContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") Div { for post in context.posts { Article { A { Img(src: post.image, alt: post.title) H2(post.title) P(post.excerpt) } .href("/\(post.slug)/") } } } .class("grid-221") } .id("blog") } .render(req) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/User/Authenticators/UserCredentialsAuthenticator.swift ================================================ import Vapor import Fluent struct UserCredentialsAuthenticator: AsyncCredentialsAuthenticator { struct Credentials: Content { let email: String let password: String } func authenticate( credentials: Credentials, for req: Request ) async throws { guard let user = try await UserAccountModel .query(on: req.db) .filter(\.$email == credentials.email) .first() else { return } do { guard try Bcrypt.verify( credentials.password, created: user.password ) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } catch { // do nothing... } } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/User/Authenticators/UserSessionAuthenticator.swift ================================================ import Vapor import Fluent struct UserSessionAuthenticator: AsyncSessionAuthenticator { typealias User = AuthenticatedUser func authenticate( sessionID: User.SessionID, for req: Request ) async throws { guard let user = try await UserAccountModel .find(sessionID, on: req.db) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/User/Controllers/UserFrontendController.swift ================================================ import Vapor struct UserFrontendController { private func renderSignInView( _ req: Request, _ form: UserLoginForm ) -> Response { let template = UserLoginTemplate( .init( icon: "⬇️", title: "Sign in", message: "Please log in with your existing account", form: form.render(req: req) ) ) return req.templates.renderHtml(template) } func signInView( _ req: Request ) async throws -> Response { renderSignInView(req, .init()) } func signInAction( _ req: Request ) async throws -> Response { /// the user is authenticated, we can store the user data inside the session too if let user = req.auth.get(AuthenticatedUser.self) { req.session.authenticate(user) return req.redirect(to: "/") } let form = UserLoginForm() try await form.process(req: req) let isValid = try await form.validate(req: req) if !isValid { form.error = "Invalid email or password." } return renderSignInView(req, form) } func signOut( _ req: Request ) throws -> Response { req.auth.logout(AuthenticatedUser.self) req.session.unauthenticate(AuthenticatedUser.self) return req.redirect(to: "/") } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/User/Database/Migrations/UserMigrations.swift ================================================ import Vapor import Fluent enum UserMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(UserAccountModel.schema) .id() .field(UserAccountModel.FieldKeys.v1.email, .string, .required) .field(UserAccountModel.FieldKeys.v1.password, .string, .required) .unique(on: UserAccountModel.FieldKeys.v1.email) .create() } func revert(on db: Database) async throws { try await db.schema(UserAccountModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let email = "root@localhost.com" let password = "ChangeMe1" let user = UserAccountModel( email: email, password: try Bcrypt.hash(password) ) try await user.create(on: db) } func revert(on db: Database) async throws { try await UserAccountModel.query(on: db).delete() } } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/User/Database/Models/UserAccountModel.swift ================================================ import Vapor import Fluent final class UserAccountModel: DatabaseModelInterface { typealias Module = UserModule struct FieldKeys { struct v1 { static var email: FieldKey { "email" } static var password: FieldKey { "password" } } } @ID() var id: UUID? @Field(key: FieldKeys.v1.email) var email: String @Field(key: FieldKeys.v1.password) var password: String init() { } init( id: UUID? = nil, email: String, password: String ) { self.id = id self.email = email self.password = password } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/User/Forms/UserLoginForm.swift ================================================ import Vapor final class UserLoginForm: AbstractForm { public convenience init() { self.init( action: .init( method: .post, url: "/sign-in/" ), submit: "Sign in" ) self.fields = createFields() } @FormComponentBuilder func createFields() -> [FormComponent] { InputField("email") .config { $0.output.context.label.required = true $0.output.context.type = .email } .validators { FormFieldValidator.required($1) FormFieldValidator.email($1) } InputField("password") .config { $0.output.context.label.required = true $0.output.context.type = .password } .validators { FormFieldValidator.required($1) } } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/User/Templates/Contexts/UserLoginContext.swift ================================================ struct UserLoginContext { let icon: String let title: String let message: String let form: TemplateRepresentable init( icon: String, title: String, message: String, form: TemplateRepresentable ) { self.icon = icon self.title = title self.message = message self.form = form } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/User/Templates/Html/UserLoginTemplate.swift ================================================ import Vapor import SwiftHtml struct UserLoginTemplate: TemplateRepresentable { var context: UserLoginContext init( _ context: UserLoginContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") context.form.render(req) } .id("user-login") .class("container") } .render(req) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/User/UserModule.swift ================================================ import Vapor struct UserModule: ModuleInterface { let router = UserRouter() func boot(_ app: Application) throws { app.migrations.add(UserMigrations.v1()) app.migrations.add(UserMigrations.seed()) app.middleware.use(UserSessionAuthenticator()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/User/UserRouter.swift ================================================ import Vapor struct UserRouter: RouteCollection { let frontendController = UserFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get("sign-in", use: frontendController.signInView) routes .grouped( UserCredentialsAuthenticator() ) .post("sign-in", use: frontendController.signInAction) routes.get("sign-out", use: frontendController.signOut) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Web/Controllers/WebFrontendController.swift ================================================ import Vapor struct WebFrontendController { func homeView(req: Request) throws -> Response { let ctx = WebHomeContext( icon: "👋", title: "Home", message: "Hi there, welcome to my page.", paragraphs: [ "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.", "Nisi ut aliquip ex ea commodo consequat.", ], link: .init( label: "Read my blog →", url: "/blog/" ) ) return req.templates.renderHtml( WebHomeTemplate(ctx) ) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Web/Templates/Contexts/WebHomeContext.swift ================================================ struct WebHomeContext { let icon: String let title: String let message: String let paragraphs: [String] let link: WebLinkContext } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Web/Templates/Contexts/WebIndexContext.swift ================================================ public struct WebIndexContext { public let title: String public init( title: String ) { self.title = title } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Web/Templates/Contexts/WebLinkContext.swift ================================================ public struct WebLinkContext { public let label: String public let url: String public init( label: String, url: String ) { self.label = label self.url = url } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Web/Templates/Html/WebHomeTemplate.swift ================================================ import Vapor import SwiftHtml struct WebHomeTemplate: TemplateRepresentable { var context: WebHomeContext init( _ context: WebHomeContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") for paragraph in context.paragraphs { P(paragraph) } WebLinkTemplate(context.link).render(req) } .id("home") .class("container") } .render(req) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg public struct WebIndexTemplate: TemplateRepresentable { public var context: WebIndexContext var body: Tag public init( _ context: WebIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Link(rel: .shortcutIcon) .href("/images/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css") Link(rel: .stylesheet) .href("/css/web.css") Title(context.title) } Body { Header { Div { A { Img(src: "/img/logo.png", alt: "Logo") } .id("site-logo") .href("/") Nav { Input() .type(.checkbox) .id("primary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("primary-menu-button") Div { A("Home") .href("/") .class("selected", req.url.path == "/") A("Blog") .href("/blog/") .class("selected", req.url.path == "/blog/") A("About") .href("#") .onClick("javascript:about();") if req.auth.has(AuthenticatedUser.self) { A("Admin") .href("/admin/") A("Sign out") .href("/sign-out/") } else { A("Sign in") .href("/sign-in/") } } .class("menu-items") } .id("primary-menu") } .id("navigation") } Main { body } Footer { Section { P { Text("This site is powered by ") A("Swift") .href("https://swift.org") .target(.blank) Text(" & ") A("Vapor") .href("https://vapor.codes") .target(.blank) Text(".") } P("myPage © 2020-2022") } } Script() .type(.javascript) .src("/js/web.js") } } .lang("en-US") } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Web/Templates/Html/WebLinkTemplate.swift ================================================ import Vapor import SwiftHtml struct WebLinkTemplate: TemplateRepresentable { var context: WebLinkContext init( _ context: WebLinkContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { A(context.label) .href(context.url) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Web/WebModule.swift ================================================ import Vapor struct WebModule: ModuleInterface { let router = WebRouter() func boot(_ app: Application) throws { try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Modules/Web/WebRouter.swift ================================================ import Vapor struct WebRouter: RouteCollection { let frontendController = WebFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get(use: frontendController.homeView) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Template/Request+Template.swift ================================================ import Vapor public extension Request { var templates: TemplateRenderer { .init(self) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Template/TemplateRenderer.swift ================================================ import Vapor import SwiftHtml public struct TemplateRenderer { var req: Request init(_ req: Request) { self.req = req } public func renderHtml( _ template: TemplateRepresentable, minify: Bool = false, indent: Int = 4 ) -> Response { let doc = Document(.html) { template.render(req) } let body = DocumentRenderer( minify: minify, indent: indent ) .render(doc) return Response( status: .ok, headers: [ "content-type": "text/html" ], body: .init(string: body) ) } } ================================================ FILE: Chapter 09/myProject/Sources/App/Template/TemplateRepresentable.swift ================================================ import Vapor import SwiftSgml public protocol TemplateRepresentable { @TagBuilder func render(_ req: Request) -> Tag } ================================================ FILE: Chapter 09/myProject/Sources/App/configure.swift ================================================ import Vapor import Fluent import FluentSQLiteDriver import Liquid import LiquidLocalDriver public func configure( _ app: Application ) throws { app.fileStorages.use( .local( publicUrl: "http://localhost:8080", publicPath: app.directory.publicDirectory, workDirectory: "assets" ), as: .local ) app.routes.defaultMaxBodySize = "10mb" let dbPath = app.directory.resourcesDirectory + "db.sqlite" app.databases.use(.sqlite(.file(dbPath)), as: .sqlite) app.middleware.use( FileMiddleware( publicDirectory: app.directory.publicDirectory ) ) app.middleware.use(ExtendPathMiddleware()) app.sessions.use(.fluent) app.migrations.add(SessionRecord.migration) app.middleware.use(app.sessions.middleware) let modules: [ModuleInterface] = [ WebModule(), UserModule(), AdminModule(), BlogModule(), ] for module in modules { try module.boot(app) } try app.autoMigrate().wait() } ================================================ FILE: Chapter 09/myProject/Sources/App/routes.swift ================================================ import Vapor import SwiftHtml func routes(_ app: Application) throws { app.routes.get { req -> Response in req.templates.renderHtml( WebIndexTemplate( WebIndexContext( title: "Home" ) ) { P("Hi there, welcome to my page!") } ) } } ================================================ FILE: Chapter 09/myProject/Sources/Run/main.swift ================================================ import App import Vapor var env = try Environment.detect() try LoggingSystem.bootstrap(from: &env) let app = Application(env) defer { app.shutdown() } try configure(app) try app.run() ================================================ FILE: Chapter 09/myProject/Tests/AppTests/AppTests.swift ================================================ @testable import App import XCTVapor final class AppTests: XCTestCase { func testHelloWorld() throws { let app = Application(.testing) defer { app.shutdown() } try configure(app) try app.test(.GET, "hello", afterResponse: { res in XCTAssertEqual(res.status, .ok) XCTAssertEqual(res.body.string, "Hello, world!") }) } } ================================================ FILE: Chapter 09/myProject/docker-compose.yml ================================================ # 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 # Stop all: docker compose down # x-shared_environment: &shared_environment LOG_LEVEL: ${LOG_LEVEL:-debug} services: app: image: myproject:latest build: context: . environment: <<: *shared_environment 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"] ================================================ FILE: Chapter 10/myProject/Dockerfile ================================================ # ================================ # Build image # ================================ FROM swift:6.0-noble 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 \ $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true) # Copy entire repo into container COPY . . # Build the application, 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 \ --product myProject \ --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)/myProject" ./ # 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:noble # 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 ["./myProject"] CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] ================================================ FILE: Chapter 10/myProject/Package.swift ================================================ // swift-tools-version:5.7 import PackageDescription let package = Package( name: "myProject", platforms: [ .macOS(.v12) ], dependencies: [ .package( url: "https://github.com/vapor/vapor", from: "4.70.0" ), .package( url: "https://github.com/vapor/fluent", from: "4.4.0" ), .package( url: "https://github.com/vapor/fluent-sqlite-driver", from: "4.1.0" ), .package( url: "https://github.com/binarybirds/liquid", from: "1.3.0" ), .package( url: "https://github.com/binarybirds/liquid-local-driver", from: "1.3.0" ), .package( url: "https://github.com/binarybirds/swift-html", from: "1.7.0" ), ], targets: [ .target(name: "App", dependencies: [ .product(name: "Vapor", package: "vapor"), .product(name: "Fluent", package: "fluent"), .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), .product(name: "Liquid", package: "liquid"), .product(name: "LiquidLocalDriver", package: "liquid-local-driver"), .product(name: "SwiftHtml", package: "swift-html"), .product(name: "SwiftSvg", package: "swift-html"), ]), .executableTarget(name: "Run", dependencies: ["App"]), .testTarget(name: "AppTests", dependencies: [ .target(name: "App"), .product(name: "XCTVapor", package: "vapor"), ]) ] ) ================================================ FILE: Chapter 10/myProject/Public/css/admin.css ================================================ tr { column-gap: 1rem; } ================================================ FILE: Chapter 10/myProject/Public/css/web.css ================================================ #blog h2 { margin: 0.5rem 0; } ================================================ FILE: Chapter 10/myProject/Public/js/admin.js ================================================ document.addEventListener("keydown", function(e) { if ( (window.navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey) && e.keyCode == 83 ) { e.preventDefault(); document.forms[0].submit(); } }, false); ================================================ FILE: Chapter 10/myProject/Public/js/web.js ================================================ function about() { alert("myPage\n\nversion 1.0.0"); } ================================================ FILE: Chapter 10/myProject/Sources/App/Extensions/Svg+MenuIcon.swift ================================================ import SwiftSvg extension Svg { static func menuIcon() -> Svg { Svg { Line(x1: 3, y1: 12, x2: 21, y2: 12) Line(x1: 3, y1: 6, x2: 21, y2: 6) Line(x1: 3, y1: 18, x2: 21, y2: 18) } .width(24) .height(24) .viewBox(minX: 0, minY: 0, width: 24, height: 24) .fill("none") .stroke("currentColor") .strokeWidth(2) .strokeLinecap("round") .strokeLinejoin("round") } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/AuthenticatedUser.swift ================================================ import Vapor public struct AuthenticatedUser { public let id: UUID public let email: String public init( id: UUID, email: String ) { self.id = id self.email = email } } extension AuthenticatedUser: SessionAuthenticatable { public var sessionID: UUID { id } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Controllers/ModelController.swift ================================================ import Vapor import Fluent public struct Name { let singular: String let plural: String init( singular: String, plural: String? = nil ) { self.singular = singular self.plural = plural ?? singular + "s" } } public protocol ModelController { associatedtype DatabaseModel: DatabaseModelInterface var modelName: Name { get } var parameterId: String { get } func identifier( _ req: Request ) throws -> UUID func findBy( _ id: UUID, on: Database ) async throws -> DatabaseModel } extension ModelController { func identifier(_ req: Request) throws -> UUID { guard let id = req.parameters.get(parameterId), let uuid = UUID(uuidString: id) else { throw Abort(.badRequest) } return uuid } func findBy( _ id: UUID, on db: Database ) async throws -> DatabaseModel { guard let model = try await DatabaseModel.find(id, on: db) else { throw Abort(.notFound) } return model } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/DatabaseModelInterface.swift ================================================ import Vapor import Fluent public protocol DatabaseModelInterface: Fluent.Model where Self.IDValue == UUID { associatedtype Module: ModuleInterface static var identifier: String { get } } public extension DatabaseModelInterface { static var schema: String { Module.identifier + "_" + identifier } static var identifier: String { String(describing: self) .dropFirst(Module.identifier.count) .dropLast(5) .lowercased() + "s" } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Extensions/ByteBuffer+Data.swift ================================================ import Vapor public extension ByteBuffer { var data: Data? { getData(at: 0, length: readableBytes) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Extensions/File+ByteBuffer.swift ================================================ import Vapor public extension File { var byteBuffer: ByteBuffer { data } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Form/AbstractForm.swift ================================================ import Vapor open class AbstractForm: FormComponent { open var action: FormAction open var fields: [FormComponent] open var error: String? open var submit: String? public init( action: FormAction = .init(), fields: [FormComponent] = [], error: String? = nil, submit: String? = nil ) { self.action = action self.fields = fields self.error = error self.submit = submit self.action.enctype = .multipart } open func load(req: Request) async throws { for field in fields { try await field.load(req: req) } } open func process(req: Request) async throws { for field in fields { try await field.process(req: req) } } open func validate(req: Request) async throws -> Bool { var result: [Bool] = [] for field in fields { result.append(try await field.validate(req: req)) } return result.filter { $0 == false }.isEmpty } open func write(req: Request) async throws { for field in fields { try await field.write(req: req) } } open func save(req: Request) async throws { for field in fields { try await field.save(req: req) } } open func read(req: Request) async throws { for field in fields { try await field.read(req: req) } } open func render(req: Request) -> TemplateRepresentable { FormTemplate(getContext(req)) } func getContext(_ req: Request) -> FormContext { .init( action: action, fields: fields.map { $0.render(req: req)}, error: error, submit: submit ) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Form/AbstractFormField.swift ================================================ import Vapor open class AbstractFormField< Input: Decodable, Output: TemplateRepresentable >: FormComponent { public var key: String public var input: Input public var output: Output public var error: String? // MARK: - event blocks public typealias FormFieldBlock = (Request, AbstractFormField) async throws -> Void public typealias FormFieldValidatorsBlock = ((Request, AbstractFormField) -> [AsyncValidator]) private var readBlock: FormFieldBlock? private var writeBlock: FormFieldBlock? private var loadBlock: FormFieldBlock? private var saveBlock: FormFieldBlock? private var validatorsBlock: FormFieldValidatorsBlock? // MARK: - init & config public init( key: String, input: Input, output: Output, error: String? = nil ) { self.key = key self.input = input self.output = output self.error = error } open func config( _ block: (AbstractFormField) -> Void ) -> Self { block(self) return self } // MARK: - Block setters open func read(_ block: @escaping FormFieldBlock) -> Self { readBlock = block return self } open func write(_ block: @escaping FormFieldBlock) -> Self { writeBlock = block return self } open func load(_ block: @escaping FormFieldBlock) -> Self { loadBlock = block return self } open func save(_ block: @escaping FormFieldBlock) -> Self { saveBlock = block return self } open func validators( @AsyncValidatorBuilder _ block: @escaping FormFieldValidatorsBlock ) -> Self { validatorsBlock = block return self } // MARK: - FormComponent open func process(req: Request) async throws { if let value = try? req.content.get(Input.self, at: key) { input = value } } open func validate(req: Request) async throws -> Bool { guard let validators = validatorsBlock else { return true } return await RequestValidator(validators(req, self)).isValid(req) } open func read(req: Request) async throws { try await readBlock?(req, self) } open func write(req: Request) async throws { try await writeBlock?(req, self) } open func load(req: Request) async throws { try await loadBlock?(req, self) } open func save(req: Request) async throws { try await saveBlock?(req, self) } open func render(req: Request) -> TemplateRepresentable { output } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Form/Fields/HiddenField.swift ================================================ import Vapor public final class HiddenField: AbstractFormField< String, HiddenFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Form/Fields/ImageField.swift ================================================ import Vapor public final class ImageField: AbstractFormField< FormImageInput, ImageFieldTemplate > { public var imageKey: String? { didSet { output.context.data.originalKey = imageKey } } public var path: String public init( _ key: String, path: String ) { self.path = path super.init( key: key, input: .init(key: key), output: .init(.init(key: key)) ) } public override func process( req: Request ) async throws { /// process input input.file = try? req.content.get( File.self, at: key ) input.data.originalKey = try? req.content.get( String.self, at: key + "OriginalKey" ) if let temporaryFileKey = try? req.content.get( String.self, at: key + "TemporaryFileKey" ), let temporaryFileName = try? req.content.get( String.self, at: key + "TemporaryFileName" ) { input.data.temporaryFile = .init( key: temporaryFileKey, name: temporaryFileName ) } input.data.shouldRemove = (try? req.content.get( Bool.self, at: key + "ShouldRemove" )) ?? false /// remove & upload file if input.data.shouldRemove { if let originalKey = input.data.originalKey { try? await req.fs.delete(key: originalKey) } } else if let file = input.file, let data = file.byteBuffer.data, !data.isEmpty { if let tmpKey = input.data.temporaryFile?.key { try? await req.fs.delete(key: tmpKey) } let key = "tmp/\(UUID().uuidString).tmp" _ = try await req.fs.upload(key: key, data: data) /// update the temporary image input.data.temporaryFile = .init( key: key, name: file.filename ) } /// update output values output.context.data = input.data } public override func write( req: Request ) async throws { imageKey = input.data.originalKey if input.data.shouldRemove { if let key = input.data.originalKey { try? await req.fs.delete(key: key) } imageKey = nil } else if let file = input.data.temporaryFile { var newKey = path + "/" + file.name if await req.fs.exists(key: newKey) { let formatter = DateFormatter() formatter.dateFormat="y-MM-dd-HH-mm-ss-" let prefix = formatter.string(from: .init()) newKey = path + "/" + prefix + file.name } _ = try await req.fs.move(key: file.key, to: newKey) input.data.temporaryFile = nil if let key = input.data.originalKey { try? await req.fs.delete(key: key) } imageKey = newKey } try await super.write(req: req) } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Form/Fields/InputField.swift ================================================ public final class InputField: AbstractFormField< String, InputFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init( key: key ) ) ) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Form/Fields/SelectField.swift ================================================ import Vapor public final class SelectField: AbstractFormField< String, SelectFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Form/Fields/TextareaField.swift ================================================ import Vapor public final class TextareaField: AbstractFormField< String, TextareaFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Form/FormAction.swift ================================================ import SwiftHtml public struct FormAction { public var method: SwiftHtml.Method public var url: String? public var enctype: SwiftHtml.Enctype? public init( method: SwiftHtml.Method = .post, url: String? = nil, enctype: SwiftHtml.Enctype? = nil ) { self.method = method self.url = url self.enctype = enctype } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Form/FormComponent.swift ================================================ import Vapor public protocol FormComponent { func load(req: Request) async throws func process(req: Request) async throws func validate(req: Request) async throws -> Bool func write(req: Request) async throws func save(req: Request) async throws func read(req: Request) async throws func render(req: Request) -> TemplateRepresentable } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Form/FormComponentBuilder.swift ================================================ @resultBuilder public enum FormComponentBuilder { public static func buildBlock( _ components: FormComponent... ) -> [FormComponent] { components } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Form/FormImageData.swift ================================================ public struct FormImageData: Codable { public struct TemporaryFile: Codable { public let key: String public let name: String public init( key: String, name: String ) { self.key = key self.name = name } } public var originalKey: String? public var temporaryFile: TemporaryFile? public var shouldRemove: Bool public init( originalKey: String? = nil, temporaryFile: TemporaryFile? = nil, shouldRemove: Bool = false ) { self.originalKey = originalKey self.temporaryFile = temporaryFile self.shouldRemove = shouldRemove } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Form/FormImageInput.swift ================================================ import Vapor public struct FormImageInput: Codable { public var key: String public var file: File? public var data: FormImageData public init( key: String, file: File? = nil, data: FormImageData? = nil ) { self.key = key self.file = file self.data = data ?? .init() } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/ModelEditorInterface.swift ================================================ import Vapor public protocol ModelEditorInterface: FormComponent { associatedtype Model: DatabaseModelInterface var model: Model { get } var form: AbstractForm { get } init(model: Model, form: AbstractForm) @FormComponentBuilder var formFields: [FormComponent] { get } } public extension ModelEditorInterface { func load(req: Request) async throws { try await form.load(req: req) } func process(req: Request) async throws { try await form.process(req: req) } func validate(req: Request) async throws -> Bool { try await form.validate(req: req) } func write(req: Request) async throws { try await form.write(req: req) } func save(req: Request) async throws { try await form.save(req: req) } func read(req: Request) async throws { try await form.read(req: req) } func render(req: Request) -> TemplateRepresentable { form.render(req: req) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/ModuleInterface.swift ================================================ import Vapor public protocol ModuleInterface { static var identifier: String { get } func boot(_ app: Application) throws } public extension ModuleInterface { func boot(_ app: Application) throws {} static var identifier: String { String(describing: self).dropLast(6).lowercased() } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Contexts/CellContext.swift ================================================ public struct CellContext { public enum `Type`: String { case text case image } public let value: String public let link: LinkContext? public let type: `Type` public init( _ value: String, link: LinkContext? = nil, type: `Type` = .text ) { self.type = type self.value = value self.link = link } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Contexts/ColumnContext.swift ================================================ public struct ColumnContext { public let key: String public let label: String public init( _ key: String, label: String? = nil ) { self.key = key self.label = label ?? key.capitalized } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Contexts/DetailContext.swift ================================================ public struct DetailContext { public enum `Type`: String { case text case image } public let key: String public let label: String public let value: String public let type: `Type` public init( _ key: String, _ value: String, label: String? = nil, type: `Type` = .text ) { self.key = key self.label = label ?? key.capitalized self.value = value self.type = type } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Contexts/FormContext.swift ================================================ public struct FormContext { public var action: FormAction public var fields: [TemplateRepresentable] public var error: String? public var submit: String? } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Contexts/HiddenFieldContext.swift ================================================ public struct HiddenFieldContext { public let key: String public var value: String? public init( key: String, value: String? = nil ) { self.key = key self.value = value } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Contexts/ImageFieldContext.swift ================================================ public struct ImageFieldContext { public let key: String public var label: LabelContext public var data: FormImageData public var previewUrl: String? public var accept: String? public var error: String? public init( key: String, label: LabelContext? = nil, data: FormImageData = .init(), previewUrl: String? = nil, accept: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.data = data self.previewUrl = previewUrl self.accept = accept self.error = error } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Contexts/InputFieldContext.swift ================================================ import SwiftHtml public struct InputFieldContext { public let key: String public var label: LabelContext public var type: SwiftHtml.Input.`Type` public var placeholder: String? public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, type: Input.`Type` = .text, placeholder: String? = nil, value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.type = type self.placeholder = placeholder self.value = value self.error = error } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Contexts/LabelContext.swift ================================================ public struct LabelContext { public var key: String public var title: String? public var required: Bool public var more: String? public init( key: String, title: String? = nil, required: Bool = false, more: String? = nil ) { self.key = key self.title = title self.required = required self.more = more } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Contexts/LinkContext.swift ================================================ import Vapor public struct LinkContext { public let label: String public let path: String public let absolute: Bool public let isBlank: Bool public let dropLast: Int public init( label: String, path: String = "", absolute: Bool = false, isBlank: Bool = false, dropLast: Int = 0 ) { self.label = label self.path = path self.absolute = absolute self.isBlank = isBlank self.dropLast = dropLast } public func url( _ req: Request, _ infix: [PathComponent] = [] ) -> String { if absolute { return path } return "/" + (req.url.path.pathComponents.dropLast(dropLast) + (infix + path.pathComponents)).string } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Contexts/OptionContext.swift ================================================ public struct OptionContext { public var key: String public var label: String public init( key: String, label: String ) { self.key = key self.label = label } } public extension OptionContext { static func yesNo() -> [OptionContext] { ["yes", "no"].map { .init(key: $0, label: $0.capitalized) } } static func trueFalse() -> [OptionContext] { [true, false].map { .init(key: String($0), label: String($0).capitalized) } } static func numbers( _ numbers: [Int] ) -> [OptionContext] { numbers.map { .init(key: String($0), label: String($0)) } } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Contexts/RowContext.swift ================================================ public struct RowContext { public let id: String public let cells: [CellContext] public init( id: String, cells: [CellContext] ) { self.id = id self.cells = cells } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Contexts/SelectFieldContext.swift ================================================ public struct SelectFieldContext { public let key: String public var label: LabelContext public var options: [OptionContext] public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, options: [OptionContext] = [], value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.options = options self.value = value self.error = error } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Contexts/TableContext.swift ================================================ public struct TableContext { public let columns: [ColumnContext] public let rows: [RowContext] public let actions: [LinkContext] public init( columns: [ColumnContext], rows: [RowContext], actions: [LinkContext] = [] ) { self.columns = columns self.rows = rows self.actions = actions } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Contexts/TextareaFieldContext.swift ================================================ public struct TextareaFieldContext { public let key: String public var label: LabelContext public var placeholder: String? public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, placeholder: String? = nil, value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.placeholder = placeholder self.value = value self.error = error } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Html/CellTemplate.swift ================================================ import Vapor import SwiftHtml public struct CellTemplate: TemplateRepresentable { var context: CellContext var rowId: String public init( _ context: CellContext, rowId: String ) { self.context = context self.rowId = rowId } @TagBuilder public func render( _ req: Request ) -> Tag { Td { switch context.type { case .text: if let link = context.link { LinkTemplate(link, pathInfix: rowId).render(req) } else { Text(context.value) } case .image: if let link = context.link { LinkTemplate(link, pathInfix: rowId) { label in Img(src: context.value, alt: label) } .render(req) } else { Img(src: context.value, alt: context.value) } } } .class("field") } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Html/DetailTemplate.swift ================================================ import Vapor import SwiftHtml public struct DetailTemplate: TemplateRepresentable { var context: DetailContext public init( _ context: DetailContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Dt(context.label) switch context.type { case .text: if context.value.isEmpty { Dd(" ") } else { Dd( context.value.replacingOccurrences( of: "\n", with: "
" ) ) } case .image: Dd { Img( src: context.value, alt: context.label ) } } } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Html/FormTemplate.swift ================================================ import Vapor import SwiftHtml public struct FormTemplate: TemplateRepresentable { var context: FormContext public init( _ context: FormContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Form { if let error = context.error { Section { P(error) .class("error") } } for field in context.fields { Section { field.render(req) } } Section { Input() .type(.submit) .value(context.submit ?? "Save") } } .method(context.action.method) .action(context.action.url) .enctype(context.action.enctype) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Html/HiddenFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct HiddenFieldTemplate: TemplateRepresentable { var context: HiddenFieldContext public init( _ context: HiddenFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Input() .type(.hidden) .name(context.key) .value(context.value) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Html/ImageFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct ImageFieldTemplate: TemplateRepresentable { public var context: ImageFieldContext public init( _ context: ImageFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { if let url = context.previewUrl { Img(src: req.fs.resolve(key: url), alt: context.key) } else { Img(src: "/img/logo.png", alt: "default post image") } LabelTemplate(context.label).render(req) Input() .type(.file) .key(context.key) .class("field") .accept(context.accept) if let temporaryFile = context.data.temporaryFile { Input() .key(context.key + "TemporaryFileKey") .value(temporaryFile.key) .type(.hidden) Input() .key(context.key + "TemporaryFileName") .value(temporaryFile.name) .type(.hidden) } if let key = context.data.originalKey { Input() .key(context.key + "OriginalKey") .value(key) .type(.hidden) } if !context.label.required { Input() .key(context.key + "ShouldRemove") .value(String(true)) .type(.checkbox) .checked(context.data.shouldRemove) Label("Remove") .for(context.key + "Remove") } if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Html/InputFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct InputFieldTemplate: TemplateRepresentable { public var context: InputFieldContext public init( _ context: InputFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Input() .type(context.type) .key(context.key) .placeholder(context.placeholder) .value(context.value) .class("field") if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Html/LabelTemplate.swift ================================================ import Vapor import SwiftHtml public struct LabelTemplate: TemplateRepresentable { var context: LabelContext public init( _ context: LabelContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Label { Text(context.title ?? context.key.capitalized) if let more = context.more { Span(more) .class("more") } if context.required { Span("*") .class("required") } }.for(context.key) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Html/LinkTemplate.swift ================================================ import Vapor import SwiftHtml public struct LinkTemplate: TemplateRepresentable { var context: LinkContext var body: Tag var pathInfix: String? public init( _ context: LinkContext, pathInfix: String? = nil, _ builder: ((String) -> Tag)? = nil ) { self.context = context self.pathInfix = pathInfix self.body = builder?(context.label) ?? Text(context.label) } @TagBuilder public func render( _ req: Request ) -> Tag { A { body } .href(context.url(req, pathInfix?.pathComponents ?? [])) .target(.blank, context.isBlank) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Html/SelectFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct SelectFieldTemplate: TemplateRepresentable { public var context: SelectFieldContext public init( _ context: SelectFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Select { for item in context.options { Option(item.label) .value(item.key) .selected(context.value == item.key) } } .name(context.key) if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Html/TableTemplate.swift ================================================ import Vapor import SwiftHtml public struct TableTemplate: TemplateRepresentable { var context: TableContext public init( _ context: TableContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Table { Thead { Tr { context.columns.map { column in Th(column.label) .id(column.key) .class("field") } context.actions.map { action in Th(action.label) .class("action") } } } Tbody { for row in context.rows { Tr { row.cells.map { CellTemplate($0, rowId: row.id).render(req) } context.actions.map { action in Td { LinkTemplate(action, pathInfix: row.id).render(req) } .class("action") } } .id(row.id) } } } } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Templates/Html/TextareaFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct TextareaFieldTemplate: TemplateRepresentable { public var context: TextareaFieldContext public init( _ context: TextareaFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Textarea(context.value) .placeholder(context.placeholder) .name(context.key) if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Validation/AsyncValidator.swift ================================================ import Vapor public protocol AsyncValidator { var key: String { get } var message: String { get } func validate( _ req: Request ) async throws -> ValidationErrorDetail? } public extension AsyncValidator { var error: ValidationErrorDetail { .init(key: key, message: message) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Validation/AsyncValidatorBuilder.swift ================================================ @resultBuilder public enum AsyncValidatorBuilder { public static func buildBlock( _ components: AsyncValidator... ) -> [AsyncValidator] { components } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Validation/FormFieldValidator+Validations.swift ================================================ import Vapor public extension FormFieldValidator where Input == String { static func required( _ field: AbstractFormField, _ message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is required" return .init(field, msg) { _, field in !field.input.isEmpty } } static func min( _ field: AbstractFormField, length: Int, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)" return .init(field, msg) { _, field in field.input.count >= length } } static func max( _ field: AbstractFormField, length: Int, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)" return .init(field, msg) { _, field in field.input.count <= length } } static func alphanumeric( _ field: AbstractFormField, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) should be only alphanumeric characters" return .init(field, msg) { _, field in !Validator .characterSet(.alphanumerics) .validate(field.input) .isFailure } } static func email( _ field: AbstractFormField, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) should be a valid email address" return .init(field, msg) { _, field in !Validator .email .validate(field.input) .isFailure } } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Validation/FormFieldValidator.swift ================================================ import Vapor public struct FormFieldValidator< Input: Decodable, Output: TemplateRepresentable >: AsyncValidator { public typealias AsyncValidationBlock = ((Request, AbstractFormField) async throws -> Bool) public let field: AbstractFormField public let message: String public let validation: AsyncValidationBlock public var key: String { field.key } public init( _ field: AbstractFormField, _ message: String, _ validation: @escaping AsyncValidationBlock ) { self.field = field self.message = message self.validation = validation } public func validate( _ req: Request ) async throws -> ValidationErrorDetail? { let isValid = try await validation(req, field) if isValid { return nil } field.error = message return error } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Validation/RequestValidator.swift ================================================ import Vapor public struct RequestValidator { public var validators: [AsyncValidator] public init( _ validators: [AsyncValidator] ) { self.validators = validators } public func validate( _ req: Request, message: String? = nil ) async throws { var result: [ValidationErrorDetail] = [] for validator in validators { if result.contains(where: { $0.key == validator.key }) { continue } if let res = try await validator.validate(req) { result.append(res) } } if !result.isEmpty { throw ValidationAbort( abort: Abort(.badRequest, reason: message), details: result ) } } public func isValid( _ req: Request ) async -> Bool { do { try await validate(req, message: nil) return true } catch { return false } } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Validation/ValidationAbort.swift ================================================ import Vapor public struct ValidationAbort: AbortError { public var abort: Abort public var message: String? public var details: [ValidationErrorDetail] public var reason: String { abort.reason } public var status: HTTPStatus { abort.status } public init( abort: Abort, message: String? = nil, details: [ValidationErrorDetail] ) { self.abort = abort self.message = message self.details = details } } ================================================ FILE: Chapter 10/myProject/Sources/App/Framework/Validation/ValidationErrorDetail.swift ================================================ import Vapor public struct ValidationErrorDetail: Codable { public var key: String public var message: String public init( key: String, message: String ) { self.key = key self.message = message } } extension ValidationErrorDetail: Content {} ================================================ FILE: Chapter 10/myProject/Sources/App/Middlewares/ExtendPathMiddleware.swift ================================================ import Vapor struct ExtendPathMiddleware: AsyncMiddleware { func respond( to req: Request, chainingTo next: AsyncResponder ) async throws -> Response { if !req.url.path.hasSuffix("/") && !req.url.path.contains(".") { return req.redirect( to: req.url.path + "/", redirectType: .permanent ) } return try await next.respond( to: req ) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Admin/AdminModule.swift ================================================ import Vapor struct AdminModule: ModuleInterface { let router = AdminRouter() func boot(_ app: Application) throws { try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Admin/AdminRouter.swift ================================================ import Vapor struct AdminRouter: RouteCollection { let controller = AdminFrontendController() func boot(routes: RoutesBuilder) throws { routes .grouped( AuthenticatedUser.redirectMiddleware( path: "/sign-in/" ) ) .get("admin", use: controller.dashboardView) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Admin/Controllers/AdminCreateController.swift ================================================ import Vapor protocol AdminCreateController: ModelController { associatedtype CreateModelEditor: ModelEditorInterface func createTemplate( _ req: Request, _ editor: CreateModelEditor ) -> TemplateRepresentable func createView( _ req: Request ) async throws -> Response func createAction( _ req: Request ) async throws -> Response func createContext( _ req: Request, _ editor: CreateModelEditor ) -> AdminEditorPageContext func createBreadcrumbs( _ req: Request ) -> [LinkContext] } extension AdminCreateController { private func render( _ req: Request, editor: CreateModelEditor ) -> Response { req.templates.renderHtml( createTemplate(req, editor) ) } func createView( _ req: Request ) async throws -> Response { let editor = CreateModelEditor( model: .init(), form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) return render(req, editor: editor) } func createAction( _ req: Request ) async throws -> Response { let model = DatabaseModel() let editor = CreateModelEditor( model: model as! CreateModelEditor.Model, form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) try await editor.process(req: req) let isValid = try await editor.validate(req: req) guard isValid else { return render(req, editor: editor) } try await editor.write(req: req) try await editor.model.create(on: req.db) try await editor.save(req: req) var components = req.url.path.pathComponents.dropLast() components += editor.model.id!.uuidString.pathComponents return req.redirect(to: "/" + components.string + "/update/") } func createTemplate( _ req: Request, _ editor: CreateModelEditor ) -> TemplateRepresentable { AdminEditorPageTemplate( createContext(req, editor) ) } func createContext( _ req: Request, _ editor: CreateModelEditor ) -> AdminEditorPageContext { let context = FormContext( action: editor.form.action, fields: editor.form.fields.map { $0.render(req: req) }, error: editor.form.error, submit: editor.form.submit ) return .init( title: "Create", form: context, breadcrumbs: createBreadcrumbs(req) ) } func createBreadcrumbs( _ req: Request ) -> [LinkContext] { [ LinkContext( label: DatabaseModel.Module.identifier.capitalized, dropLast: 2 ), LinkContext( label: modelName.plural.capitalized, dropLast: 1 ), ] } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Admin/Controllers/AdminDeleteController.swift ================================================ import Vapor final class DeleteForm: AbstractForm { init() { super.init() self.action.method = .post self.submit = "Delete" } } protocol AdminDeleteController: ModelController { func deleteView( _ req: Request ) async throws -> Response func deleteAction( _ req: Request ) async throws -> Response func deleteTemplate( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> TemplateRepresentable func deleteInfo( _ model: DatabaseModel ) -> String func deleteContext( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> AdminDeletePageContext } extension AdminDeleteController { func deleteView( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let form = DeleteForm() return req.templates.renderHtml( deleteTemplate(req, model, form) ) } func deleteAction( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) try await model.delete(on: req.db) var url = req.url.path if let redirect = try? req.query.get(String.self, at: "redirect") { url = redirect } return req.redirect(to: url) } func deleteTemplate( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> TemplateRepresentable { AdminDeletePageTemplate( deleteContext(req, model, form) ) } func deleteContext( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> AdminDeletePageContext { .init( title: "Delete", name: deleteInfo(model), type: "model", form: form.getContext(req) ) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Admin/Controllers/AdminDetailController.swift ================================================ import Vapor protocol AdminDetailController: ModelController { func detailView( _ req: Request ) async throws -> Response func detailTemplate( _ req: Request, _ model: DatabaseModel ) -> TemplateRepresentable func detailFields( for model: DatabaseModel ) -> [DetailContext] func detailContext( _ req: Request, _ model: DatabaseModel ) -> AdminDetailPageContext func detailBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func detailNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] } extension AdminDetailController { func detailView( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) return req.templates.renderHtml(detailTemplate(req, model)) } func detailTemplate( _ req: Request, _ model: DatabaseModel ) -> TemplateRepresentable { AdminDetailPageTemplate(detailContext(req, model)) } func detailContext( _ req: Request, _ model: DatabaseModel ) -> AdminDetailPageContext { let path = "/delete/?redirect=" + req.url.path.pathComponents.dropLast().string + "&cancel=" + req.url.path return .init( title: "Details", fields: detailFields(for: model), navigation: detailNavigation(req, model), breadcrumbs: detailBreadcrumbs(req, model), actions: [ LinkContext( label: "Delete", path: path ), ] ) } func detailBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: DatabaseModel.Module.identifier.capitalized, dropLast: 2 ), LinkContext( label: modelName.plural.capitalized, dropLast: 1 ), ] } func detailNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: "Update", path: "update" ), ] } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Admin/Controllers/AdminFrontendController.swift ================================================ import Vapor struct AdminFrontendController { func dashboardView( req: Request ) throws -> Response { let user = try req.auth.require(AuthenticatedUser.self) let template = AdminDashboardTemplate( .init( icon: "👋", title: "Dashboard", message: "Hello \(user.email), welcome to the CMS." ) ) return req.templates.renderHtml(template) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Admin/Controllers/AdminListController.swift ================================================ import Vapor protocol AdminListController: ModelController { func list(_ req: Request) async throws -> [DatabaseModel] func listView(_ req: Request) async throws -> Response func listColumns() -> [ColumnContext] func listCells(for model: DatabaseModel) -> [CellContext] func listNavigation(_ req: Request) -> [LinkContext] func listBreadcrumbs(_ req: Request) -> [LinkContext] func listContext( _ req: Request, _ list: [DatabaseModel] ) -> AdminListPageContext func listTemplate( _ req: Request, _ list: [DatabaseModel] ) -> TemplateRepresentable } extension AdminListController { func list(_ req: Request) async throws -> [DatabaseModel] { try await DatabaseModel.query(on: req.db).all() } func listView(_ req: Request) async throws -> Response { let list = try await list(req) let template = listTemplate(req, list) return req.templates.renderHtml(template) } func listNavigation(_ req: Request) -> [LinkContext] { [ LinkContext(label: "Create",path: "create") ] } func listBreadcrumbs(_ req: Request) -> [LinkContext] { [ LinkContext( label: DatabaseModel.Module.identifier.capitalized, dropLast: 1 ) ] } func listContext( _ req: Request, _ list: [DatabaseModel] ) -> AdminListPageContext { let rows = list.map { RowContext(id: $0.id!.uuidString, cells: listCells(for: $0)) } let table = TableContext(columns: listColumns(), rows: rows, actions: [ LinkContext(label: "Update", path: "update"), LinkContext(label: "Delete", path: "delete") ]) return .init( title: "List", table: table, navigation: listNavigation(req), breadcrumbs: listBreadcrumbs(req) ) } func listTemplate( _ req: Request, _ list: [DatabaseModel] ) -> TemplateRepresentable { AdminListPageTemplate(listContext(req, list)) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Admin/Controllers/AdminUpdateController.swift ================================================ import Vapor protocol AdminUpdateController: ModelController { associatedtype UpdateModelEditor: ModelEditorInterface func updateView( _ req: Request ) async throws -> Response func updateAction( _ req: Request ) async throws -> Response func updateTemplate( _ req: Request, _ editor: UpdateModelEditor ) async -> TemplateRepresentable func updateContext( _ req: Request, _ editor: UpdateModelEditor ) async -> AdminEditorPageContext func updateBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func updateNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] } extension AdminUpdateController { private func render( _ req: Request, editor: UpdateModelEditor ) async -> Response { req.templates.renderHtml( await updateTemplate(req, editor) ) } func updateView( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let editor = UpdateModelEditor( model: model as! UpdateModelEditor.Model, form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) try await editor.read(req: req) return await render(req, editor: editor) } func updateAction( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let editor = UpdateModelEditor( model: model as! UpdateModelEditor.Model, form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) try await editor.process(req: req) let isValid = try await editor.validate(req: req) guard isValid else { return await render(req, editor: editor) } try await editor.write(req: req) try await editor.model.update(on: req.db) try await editor.save(req: req) return req.redirect(to: req.url.path) } func updateTemplate( _ req: Request, _ editor: UpdateModelEditor ) async -> TemplateRepresentable { await AdminEditorPageTemplate( updateContext(req, editor) ) } func updateContext( _ req: Request, _ editor: UpdateModelEditor ) async -> AdminEditorPageContext { let path = "delete/?redirect=" + req.url.path.pathComponents.dropLast(2).string + "&cancel=" + req.url.path let model = editor.model as! DatabaseModel return .init( title: "Update", form: editor.form.getContext(req), navigation: updateNavigation(req, model), breadcrumbs: updateBreadcrumbs(req, model), actions: [ LinkContext( label: "Delete", path: path, dropLast: 1 ), ] ) } func updateBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: DatabaseModel.Module.identifier.capitalized, dropLast: 3 ), LinkContext( label: modelName.plural.capitalized, dropLast: 2 ), ] } func updateNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: "Details", dropLast: 1 ), ] } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDashboardContext.swift ================================================ struct AdminDashboardContext { let icon: String let title: String let message: String } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDeletePageContext.swift ================================================ public struct AdminDeletePageContext { public let title: String public let name: String public let type: String public let form: FormContext public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public init( title: String, name: String, type: String, form: FormContext, navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [] ) { self.title = title self.name = name self.type = type self.form = form self.navigation = navigation self.breadcrumbs = breadcrumbs } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDetailPageContext.swift ================================================ public struct AdminDetailPageContext { public let title: String public let fields: [DetailContext] public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public let actions: [LinkContext] public init( title: String, fields: [DetailContext], navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [], actions: [LinkContext] = [] ) { self.title = title self.fields = fields self.navigation = navigation self.breadcrumbs = breadcrumbs self.actions = actions } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminEditorPageContext.swift ================================================ public struct AdminEditorPageContext { public let title: String public let form: FormContext public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public let actions: [LinkContext] public init( title: String, form: FormContext, navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [], actions: [LinkContext] = [] ) { self.title = title self.form = form self.navigation = navigation self.breadcrumbs = breadcrumbs self.actions = actions } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminIndexContext.swift ================================================ public struct AdminIndexContext { public let title: String public let breadcrumbs: [LinkContext] public init( title: String, breadcrumbs: [LinkContext] = [] ) { self.title = title self.breadcrumbs = breadcrumbs } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminListPageContext.swift ================================================ public struct AdminListPageContext { public let title: String public let table: TableContext public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public init( title: String, table: TableContext, navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [] ) { self.title = title self.table = table self.navigation = navigation self.breadcrumbs = breadcrumbs } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDashboardTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminDashboardTemplate: TemplateRepresentable { var context: AdminDashboardContext init( _ context: AdminDashboardContext ) { self.context = context } @TagBuilder func render(_ req: Request) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } Nav { H2("Blog") Ul { Li { A("Posts") .href("/admin/blog/posts/") } Li { A("Categories") .href("/admin/blog/categories/") } } } } .id("dashboard") .class("container") } .render(req) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDeletePageTemplate.swift ================================================ import Vapor import SwiftHtml public struct AdminDeletePageTemplate: TemplateRepresentable { var context: AdminDeletePageContext public init( _ context: AdminDeletePageContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { Span("🗑") .class("icon") H1(context.title) P("You are about to permanently delete the
`\(context.name)` \(context.type).") FormTemplate(context.form).render(req) A("Cancel") .href((try? req.query.get(String.self, at: "cancel")) ?? "#") .class(["button", "cancel"]) } .class(["lead", "container", "center"]) } .render(req) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDetailPageTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminDetailPageTemplate: TemplateRepresentable { var context: AdminDetailPageContext init( _ context: AdminDetailPageContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { Div { H1(context.title) for item in context.navigation { LinkTemplate(item).render(req) } } .class("lead") Dl { for item in context.fields { DetailTemplate(item).render(req) } } Section { for item in context.actions { LinkTemplate(item).render(req) } } } .class("container") } .render(req) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Admin/Templates/Html/AdminEditorPageTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminEditorPageTemplate: TemplateRepresentable { var context: AdminEditorPageContext init( _ context: AdminEditorPageContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { Div { H1(context.title) for item in context.navigation { LinkTemplate(item).render(req) } } .class("lead") FormTemplate(context.form).render(req) Section { for item in context.actions { LinkTemplate(item).render(req) } } } .class("container") } .render(req) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Admin/Templates/Html/AdminIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg public struct AdminIndexTemplate: TemplateRepresentable { public var context: AdminIndexContext var body: Tag public init( _ context: AdminIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Meta() .name("robots") .content("noindex") Link(rel: .shortcutIcon) .href("/images/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css") Link(rel: .stylesheet) .href("/css/admin.css") Title(context.title) } Body { Div { A { Img(src: "/img/logo.png", alt: "Logo") .title("Logo") .style("width: 300px") } .href("/") Nav { Input() .type(.checkbox) .id("secondary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("secondary-menu-button") Div { A("Sign out") .href("/sign-out/") } .class("menu-items") } .id("secondary-menu") } .id("navigation") Div { Nav { A("Admin") .href("/admin/") for breadcrumb in context.breadcrumbs { LinkTemplate(breadcrumb).render(req) } } } .class("breadcrumb") Main { body } Script() .type(.javascript) .src("/js/admin.js") } } .lang("en-US") } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Admin/Templates/Html/AdminListPageTemplate.swift ================================================ import Vapor import SwiftHtml public struct AdminListPageTemplate: TemplateRepresentable { var context: AdminListPageContext public init( _ context: AdminListPageContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { H1(context.title) P { context.navigation.map { LinkTemplate($0).render(req) } } } .class("lead") if context.table.rows.isEmpty { Div { Span("🔍") .class("icon") H2("Oh no") P("This list is empty right now.") A("Try again →") .href(req.url.path) .class("button-1") } .class(["lead", "container", "center"]) } else { TableTemplate(context.table).render(req) } } .render(req) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/BlogModule.swift ================================================ import Vapor struct BlogModule: ModuleInterface { let router = BlogRouter() func boot(_ app: Application) throws { app.migrations.add(BlogMigrations.v1()) app.migrations.add(BlogMigrations.seed()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/BlogRouter.swift ================================================ import Vapor struct BlogRouter: RouteCollection { let controller = BlogFrontendController() let postAdminController = BlogPostAdminController() let categoryAdminController = BlogCategoryAdminController() func boot(routes: RoutesBuilder) throws { routes.get("blog", use: controller.blogView) routes.get(.anything, use: controller.postView) let blog = routes .grouped(AuthenticatedUser.redirectMiddleware(path: "/")) .grouped("admin", "blog") let categories = blog.grouped("categories") categories.get(use: categoryAdminController.listView) categories.get("create", use: categoryAdminController.createView) categories.post("create", use: categoryAdminController.createAction) let categoryId = categories.grouped(":categoryId") categoryId.get(use: categoryAdminController.detailView) categoryId.get("update", use: categoryAdminController.updateView) categoryId.post("update", use: categoryAdminController.updateAction) categoryId.get("delete", use: categoryAdminController.deleteView) categoryId.post("delete", use: categoryAdminController.deleteAction) let posts = blog.grouped("posts") posts.get(use: postAdminController.listView) let postId = posts.grouped(":postId") postId.get(use: postAdminController.detailView) posts.get("create", use: postAdminController.createView) posts.post("create", use: postAdminController.createAction) postId.get("update", use: postAdminController.updateView) postId.post("update", use: postAdminController.updateAction) postId.get("delete", use: postAdminController.deleteView) postId.post("delete", use: postAdminController.deleteAction) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/Controllers/BlogCategoryAdminController.swift ================================================ import Vapor import Fluent struct BlogCategoryAdminController: AdminListController, AdminDetailController, AdminCreateController, AdminUpdateController, AdminDeleteController { typealias DatabaseModel = BlogCategoryModel typealias CreateModelEditor = BlogCategoryEditor typealias UpdateModelEditor = BlogCategoryEditor let modelName: Name = .init( singular: "category", plural: "categories" ) let parameterId: String = "categoryId" func listColumns() -> [ColumnContext] { [ .init("title"), ] } func listCells( for model: DatabaseModel ) -> [CellContext] { [ .init( model.title, link: .init(label: model.title) ), ] } func detailFields( for model: DatabaseModel ) -> [DetailContext] { [ .init("title", model.title), ] } func deleteInfo( _ model: DatabaseModel ) -> String { model.title } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift ================================================ import Vapor import Fluent struct BlogFrontendController { func blogView(req: Request) async throws -> Response { let posts = try await BlogPostModel .query(on: req.db) .sort(\.$date, .descending) .all() let api = BlogPostApiController() let list = posts.map { api.mapList($0) } let ctx = BlogPostsContext( icon: "🔥", title: "Blog", message: "Hot news and stories about everything.", posts: list ) return req.templates.renderHtml(BlogPostsTemplate(ctx)) } func postView(req: Request) async throws -> Response { let slug = req.url.path.trimmingCharacters( in: .init(charactersIn: "/") ) guard let post = try await BlogPostModel .query(on: req.db) .filter(\.$slug == slug) .with(\.$category) .first() else { return req.redirect(to: "/") } let api = BlogPostApiController() let ctx = BlogPostContext(post: api.mapDetail(post)) return req.templates.renderHtml(BlogPostTemplate(ctx)) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/Controllers/BlogPostAdminController.swift ================================================ import Vapor import Fluent struct BlogPostAdminController: AdminListController, AdminDetailController, AdminCreateController, AdminUpdateController, AdminDeleteController { typealias DatabaseModel = BlogPostModel typealias CreateModelEditor = BlogPostEditor typealias UpdateModelEditor = BlogPostEditor let modelName: Name = .init(singular: "post") let parameterId: String = "postId" func listColumns() -> [ColumnContext] { [ .init("image"), .init("title"), ] } func listCells(for model: DatabaseModel) -> [CellContext] { [ .init(model.imageKey, type: .image), .init(model.title, link: .init(label: model.title)), ] } func detailFields(for model: DatabaseModel) -> [DetailContext] { [ .init("image", model.imageKey, type: .image), .init("title", model.title), ] } func deleteInfo( _ model: DatabaseModel ) -> String { model.title } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/Controllers/BlogPostApiController.swift ================================================ import Vapor struct BlogPostApiController { func mapList(_ model: BlogPostModel) -> Blog.Post.List { .init( id: model.id!, title: model.title, slug: model.slug, image: model.imageKey, excerpt: model.excerpt, date: model.date ) } func mapDetail(_ model: BlogPostModel) -> Blog.Post.Detail { .init( id: model.id!, title: model.title, slug: model.slug, image: model.imageKey, excerpt: model.excerpt, date: model.date, category: .init( id: model.category.id!, title: model.category.title ), content: model.content ) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/Database/Migrations/BlogMigrations.swift ================================================ import Foundation import Fluent enum BlogMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema) .id() .field(BlogCategoryModel.FieldKeys.v1.title, .string, .required) .create() try await db.schema(BlogPostModel.schema) .id() .field(BlogPostModel.FieldKeys.v1.title, .string, .required) .field(BlogPostModel.FieldKeys.v1.slug, .string, .required) .field(BlogPostModel.FieldKeys.v1.imageKey, .string, .required) .field(BlogPostModel.FieldKeys.v1.excerpt, .data, .required) .field(BlogPostModel.FieldKeys.v1.date, .datetime, .required) .field(BlogPostModel.FieldKeys.v1.content, .data, .required) .field(BlogPostModel.FieldKeys.v1.categoryId, .uuid) .foreignKey( BlogPostModel.FieldKeys.v1.categoryId, references: BlogCategoryModel.schema, .id, onDelete: DatabaseSchema.ForeignKeyAction.setNull, onUpdate: .cascade ) .unique(on: BlogPostModel.FieldKeys.v1.slug) .create() } func revert(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema).delete() try await db.schema(BlogPostModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let categories = (1...4).map { index in BlogCategoryModel(title: "Sample category #\(index)") } try await categories.create(on: db) try await (1...9).map { index in BlogPostModel( id: nil, title: "Sample post #\(index)", slug: "sample-post-\(index)", imageKey: "/img/posts/\(String(format: "%02d", index + 1)).jpg", excerpt: "Lorem ipsum", date: Date().addingTimeInterval(-Double.random(in: 0...(86400 * 60))), content: "Lorem ipsum dolor sit amet.", categoryId: categories[Int.random(in: 0.. [FormComponent] { ImageField("image", path: "blog/post") .read { [unowned self] in $1.output.context.previewUrl = model.imageKey ($1 as! ImageField).imageKey = model.imageKey } .write { [unowned self] in model.imageKey = ($1 as! ImageField).imageKey ?? "" } InputField("slug") .config { $0.output.context.label.required = true } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = model.slug } .write { [unowned self] in model.slug = $1.input } InputField("title") .config { $0.output.context.label.required = true } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = model.title } .write { [unowned self] in model.title = $1.input } InputField("date") .config { $0.output.context.label.required = true $0.output.context.value = dateFormatter.string(from: Date()) } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = dateFormatter.string(from: model.date) } .write { [unowned self] in model.date = dateFormatter.date(from: $1.input) ?? Date() } TextareaField("excerpt") .read { [unowned self] in $1.output.context.value = model.excerpt } .write { [unowned self] in model.excerpt = $1.input } TextareaField("content") .read { [unowned self] in $1.output.context.value = model.content } .write { [unowned self] in model.content = $1.input } SelectField("category") .load { req, field in let categories = try await BlogCategoryModel .query(on: req.db) .all() field.output.context.options = categories.map { OptionContext(key: $0.id!.uuidString, label: $0.title) } } .read { [unowned self] req, field in field.output.context.value = model.$category.id.uuidString } .write { [unowned self] req, field in if let uuid = UUID(uuidString: field.input), let category = try await BlogCategoryModel .find(uuid, on: req.db) { model.$category.id = category.id! } } } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/Objects/Blog.swift ================================================ enum Blog { enum Post { } enum Category { } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/Objects/BlogCategory.swift ================================================ import Foundation extension Blog.Category { struct List: Codable { let id: UUID let title: String } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/Objects/BlogPost.swift ================================================ import Foundation extension Blog.Post { struct List: Codable { let id: UUID let title: String let slug: String let image: String let excerpt: String let date: Date } struct Detail: Codable { let id: UUID let title: String let slug: String let image: String let excerpt: String let date: Date let category: Blog.Category.List let content: String } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminDeleteContext.swift ================================================ struct BlogPostAdminDeleteContext { let title: String let name: String let type: String } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminDetailContext.swift ================================================ struct BlogPostAdminDetailContext { let title: String let detail: Blog.Post.Detail } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminEditContext.swift ================================================ struct BlogPostAdminEditContext { let title: String let form: TemplateRepresentable } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminListContext.swift ================================================ struct BlogPostAdminListContext { let title: String let list: [Blog.Post.List] } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostContext.swift ================================================ struct BlogPostContext { let post: Blog.Post.Detail } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostsContext.swift ================================================ struct BlogPostsContext { let icon: String let title: String let message: String let posts: [Blog.Post.List] } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminDeleteTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminDeleteTemplate: TemplateRepresentable { var context: BlogPostAdminDeleteContext init( _ context: BlogPostAdminDeleteContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Span("🗑") .class("icon") H1(context.title) P("You are about to permanently delete the
`\(context.name)` \(context.type).") Form { Input() .type(.submit) .class(["button", "destructive"]) .style("display: inline") .value("Delete") A("Cancel") .href("/admin/blog/posts/") .class(["button", "cancel"]) } .method(.post) .id("delete-form") } .class(["lead", "container", "center"]) } .render(req) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminDetailTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminDetailTemplate: TemplateRepresentable { var context: BlogPostAdminDetailContext init( _ context: BlogPostAdminDetailContext ) { self.context = context } var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .short return formatter }() @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") Dl { Dt("Image") Dd { Img( src: context.detail.image, alt: context.detail.title ) } Dt("Title") Dd(context.detail.title) Dt("Excerpt") Dd(context.detail.excerpt) Dt("Date") Dd(dateFormatter.string(from: context.detail.date)) Dt("Content") Dd(context.detail.content) } } .id("detail") .class("container") } .render(req) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminEditTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminEditTemplate: TemplateRepresentable { var context: BlogPostAdminEditContext init( _ context: BlogPostAdminEditContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") context.form.render(req) } .id("edit") .class("container") } .render(req) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminListTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminListTemplate: TemplateRepresentable { var context: BlogPostAdminListContext init( _ context: BlogPostAdminListContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") Table { Thead { Tr { Th("Image") Th("Title") Th("Preview") } } Tbody { for item in context.list { Tr { Td { Img(src: item.image, alt: item.title) } Td { A(item.title) .href("/admin/blog/posts/" + item.id.uuidString + "/") } Td { A("Preview") .href("/" + item.slug + "/") } } } } } } .id("list") } .render(req) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostTemplate: TemplateRepresentable { var context: BlogPostContext init( _ context: BlogPostContext ) { self.context = context } var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .short return formatter }() @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.post.title) ) { Div { Section { P(dateFormatter.string(from: context.post.date)) H1(context.post.title) P(context.post.excerpt) } .class(["lead", "container"]) Img(src: context.post.image, alt: context.post.title) Article { Text(context.post.content) } .class("container") } .id("post") } .render(req) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostsTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostsTemplate: TemplateRepresentable { var context: BlogPostsContext init( _ context: BlogPostsContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") Div { for post in context.posts { Article { A { Img(src: post.image, alt: post.title) H2(post.title) P(post.excerpt) } .href("/\(post.slug)/") } } } .class("grid-221") } .id("blog") } .render(req) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/User/Authenticators/UserCredentialsAuthenticator.swift ================================================ import Vapor import Fluent struct UserCredentialsAuthenticator: AsyncCredentialsAuthenticator { struct Credentials: Content { let email: String let password: String } func authenticate( credentials: Credentials, for req: Request ) async throws { guard let user = try await UserAccountModel .query(on: req.db) .filter(\.$email == credentials.email) .first() else { return } do { guard try Bcrypt.verify( credentials.password, created: user.password ) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } catch { // do nothing... } } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/User/Authenticators/UserSessionAuthenticator.swift ================================================ import Vapor import Fluent struct UserSessionAuthenticator: AsyncSessionAuthenticator { typealias User = AuthenticatedUser func authenticate( sessionID: User.SessionID, for req: Request ) async throws { guard let user = try await UserAccountModel .find(sessionID, on: req.db) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/User/Controllers/UserFrontendController.swift ================================================ import Vapor struct UserFrontendController { private func renderSignInView( _ req: Request, _ form: UserLoginForm ) -> Response { let template = UserLoginTemplate( .init( icon: "⬇️", title: "Sign in", message: "Please log in with your existing account", form: form.render(req: req) ) ) return req.templates.renderHtml(template) } func signInView( _ req: Request ) async throws -> Response { renderSignInView(req, .init()) } func signInAction( _ req: Request ) async throws -> Response { /// the user is authenticated, we can store the user data inside the session too if let user = req.auth.get(AuthenticatedUser.self) { req.session.authenticate(user) return req.redirect(to: "/") } let form = UserLoginForm() try await form.process(req: req) let isValid = try await form.validate(req: req) if !isValid { form.error = "Invalid email or password." } return renderSignInView(req, form) } func signOut( _ req: Request ) throws -> Response { req.auth.logout(AuthenticatedUser.self) req.session.unauthenticate(AuthenticatedUser.self) return req.redirect(to: "/") } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/User/Database/Migrations/UserMigrations.swift ================================================ import Vapor import Fluent enum UserMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(UserAccountModel.schema) .id() .field(UserAccountModel.FieldKeys.v1.email, .string, .required) .field(UserAccountModel.FieldKeys.v1.password, .string, .required) .unique(on: UserAccountModel.FieldKeys.v1.email) .create() } func revert(on db: Database) async throws { try await db.schema(UserAccountModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let email = "root@localhost.com" let password = "ChangeMe1" let user = UserAccountModel( email: email, password: try Bcrypt.hash(password) ) try await user.create(on: db) } func revert(on db: Database) async throws { try await UserAccountModel.query(on: db).delete() } } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/User/Database/Models/UserAccountModel.swift ================================================ import Vapor import Fluent final class UserAccountModel: DatabaseModelInterface { typealias Module = UserModule struct FieldKeys { struct v1 { static var email: FieldKey { "email" } static var password: FieldKey { "password" } } } @ID() var id: UUID? @Field(key: FieldKeys.v1.email) var email: String @Field(key: FieldKeys.v1.password) var password: String init() { } init( id: UUID? = nil, email: String, password: String ) { self.id = id self.email = email self.password = password } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/User/Forms/UserLoginForm.swift ================================================ import Vapor final class UserLoginForm: AbstractForm { public convenience init() { self.init( action: .init( method: .post, url: "/sign-in/" ), submit: "Sign in" ) self.fields = createFields() } @FormComponentBuilder func createFields() -> [FormComponent] { InputField("email") .config { $0.output.context.label.required = true $0.output.context.type = .email } .validators { FormFieldValidator.required($1) FormFieldValidator.email($1) } InputField("password") .config { $0.output.context.label.required = true $0.output.context.type = .password } .validators { FormFieldValidator.required($1) } } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/User/Templates/Contexts/UserLoginContext.swift ================================================ struct UserLoginContext { let icon: String let title: String let message: String let form: TemplateRepresentable init( icon: String, title: String, message: String, form: TemplateRepresentable ) { self.icon = icon self.title = title self.message = message self.form = form } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/User/Templates/Html/UserLoginTemplate.swift ================================================ import Vapor import SwiftHtml struct UserLoginTemplate: TemplateRepresentable { var context: UserLoginContext init( _ context: UserLoginContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") context.form.render(req) } .id("user-login") .class("container") } .render(req) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/User/UserModule.swift ================================================ import Vapor struct UserModule: ModuleInterface { let router = UserRouter() func boot(_ app: Application) throws { app.migrations.add(UserMigrations.v1()) app.migrations.add(UserMigrations.seed()) app.middleware.use(UserSessionAuthenticator()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/User/UserRouter.swift ================================================ import Vapor struct UserRouter: RouteCollection { let frontendController = UserFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get("sign-in", use: frontendController.signInView) routes .grouped( UserCredentialsAuthenticator() ) .post("sign-in", use: frontendController.signInAction) routes.get("sign-out", use: frontendController.signOut) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Web/Controllers/WebFrontendController.swift ================================================ import Vapor struct WebFrontendController { func homeView(req: Request) throws -> Response { let ctx = WebHomeContext( icon: "👋", title: "Home", message: "Hi there, welcome to my page.", paragraphs: [ "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.", "Nisi ut aliquip ex ea commodo consequat.", ], link: .init( label: "Read my blog →", url: "/blog/" ) ) return req.templates.renderHtml( WebHomeTemplate(ctx) ) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Web/Templates/Contexts/WebHomeContext.swift ================================================ struct WebHomeContext { let icon: String let title: String let message: String let paragraphs: [String] let link: WebLinkContext } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Web/Templates/Contexts/WebIndexContext.swift ================================================ public struct WebIndexContext { public let title: String public init( title: String ) { self.title = title } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Web/Templates/Contexts/WebLinkContext.swift ================================================ public struct WebLinkContext { public let label: String public let url: String public init( label: String, url: String ) { self.label = label self.url = url } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Web/Templates/Html/WebHomeTemplate.swift ================================================ import Vapor import SwiftHtml struct WebHomeTemplate: TemplateRepresentable { var context: WebHomeContext init( _ context: WebHomeContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") for paragraph in context.paragraphs { P(paragraph) } WebLinkTemplate(context.link).render(req) } .id("home") .class("container") } .render(req) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg public struct WebIndexTemplate: TemplateRepresentable { public var context: WebIndexContext var body: Tag public init( _ context: WebIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Link(rel: .shortcutIcon) .href("/images/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css") Link(rel: .stylesheet) .href("/css/web.css") Title(context.title) } Body { Header { Div { A { Img(src: "/img/logo.png", alt: "Logo") } .id("site-logo") .href("/") Nav { Input() .type(.checkbox) .id("primary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("primary-menu-button") Div { A("Home") .href("/") .class("selected", req.url.path == "/") A("Blog") .href("/blog/") .class("selected", req.url.path == "/blog/") A("About") .href("#") .onClick("javascript:about();") if req.auth.has(AuthenticatedUser.self) { A("Admin") .href("/admin/") A("Sign out") .href("/sign-out/") } else { A("Sign in") .href("/sign-in/") } } .class("menu-items") } .id("primary-menu") } .id("navigation") } Main { body } Footer { Section { P { Text("This site is powered by ") A("Swift") .href("https://swift.org") .target(.blank) Text(" & ") A("Vapor") .href("https://vapor.codes") .target(.blank) Text(".") } P("myPage © 2020-2022") } } Script() .type(.javascript) .src("/js/web.js") } } .lang("en-US") } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Web/Templates/Html/WebLinkTemplate.swift ================================================ import Vapor import SwiftHtml struct WebLinkTemplate: TemplateRepresentable { var context: WebLinkContext init( _ context: WebLinkContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { A(context.label) .href(context.url) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Web/WebModule.swift ================================================ import Vapor struct WebModule: ModuleInterface { let router = WebRouter() func boot(_ app: Application) throws { try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Modules/Web/WebRouter.swift ================================================ import Vapor struct WebRouter: RouteCollection { let frontendController = WebFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get(use: frontendController.homeView) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Template/Request+Template.swift ================================================ import Vapor public extension Request { var templates: TemplateRenderer { .init(self) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Template/TemplateRenderer.swift ================================================ import Vapor import SwiftHtml public struct TemplateRenderer { var req: Request init(_ req: Request) { self.req = req } public func renderHtml( _ template: TemplateRepresentable, minify: Bool = false, indent: Int = 4 ) -> Response { let doc = Document(.html) { template.render(req) } let body = DocumentRenderer( minify: minify, indent: indent ) .render(doc) return Response( status: .ok, headers: [ "content-type": "text/html" ], body: .init(string: body) ) } } ================================================ FILE: Chapter 10/myProject/Sources/App/Template/TemplateRepresentable.swift ================================================ import Vapor import SwiftSgml public protocol TemplateRepresentable { @TagBuilder func render(_ req: Request) -> Tag } ================================================ FILE: Chapter 10/myProject/Sources/App/configure.swift ================================================ import Vapor import Fluent import FluentSQLiteDriver import Liquid import LiquidLocalDriver public func configure( _ app: Application ) throws { app.fileStorages.use( .local( publicUrl: "http://localhost:8080", publicPath: app.directory.publicDirectory, workDirectory: "assets" ), as: .local ) app.routes.defaultMaxBodySize = "10mb" let dbPath = app.directory.resourcesDirectory + "db.sqlite" app.databases.use(.sqlite(.file(dbPath)), as: .sqlite) app.middleware.use( FileMiddleware( publicDirectory: app.directory.publicDirectory ) ) app.middleware.use(ExtendPathMiddleware()) app.sessions.use(.fluent) app.migrations.add(SessionRecord.migration) app.middleware.use(app.sessions.middleware) let modules: [ModuleInterface] = [ WebModule(), UserModule(), AdminModule(), BlogModule(), ] for module in modules { try module.boot(app) } try app.autoMigrate().wait() } ================================================ FILE: Chapter 10/myProject/Sources/App/routes.swift ================================================ import Vapor import SwiftHtml func routes(_ app: Application) throws { app.routes.get { req -> Response in req.templates.renderHtml( WebIndexTemplate( WebIndexContext( title: "Home" ) ) { P("Hi there, welcome to my page!") } ) } } ================================================ FILE: Chapter 10/myProject/Sources/Run/main.swift ================================================ import App import Vapor var env = try Environment.detect() try LoggingSystem.bootstrap(from: &env) let app = Application(env) defer { app.shutdown() } try configure(app) try app.run() ================================================ FILE: Chapter 10/myProject/Tests/AppTests/AppTests.swift ================================================ @testable import App import XCTVapor final class AppTests: XCTestCase { func testHelloWorld() throws { let app = Application(.testing) defer { app.shutdown() } try configure(app) try app.test(.GET, "hello", afterResponse: { res in XCTAssertEqual(res.status, .ok) XCTAssertEqual(res.body.string, "Hello, world!") }) } } ================================================ FILE: Chapter 10/myProject/docker-compose.yml ================================================ # 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 # Stop all: docker compose down # x-shared_environment: &shared_environment LOG_LEVEL: ${LOG_LEVEL:-debug} services: app: image: myproject:latest build: context: . environment: <<: *shared_environment 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"] ================================================ FILE: Chapter 11/myProject/Dockerfile ================================================ # ================================ # Build image # ================================ FROM swift:6.0-noble 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 \ $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true) # Copy entire repo into container COPY . . # Build the application, 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 \ --product myProject \ --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)/myProject" ./ # 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:noble # 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 ["./myProject"] CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] ================================================ FILE: Chapter 11/myProject/Package.swift ================================================ // swift-tools-version:5.7 import PackageDescription let package = Package( name: "myProject", platforms: [ .macOS(.v12) ], dependencies: [ .package( url: "https://github.com/vapor/vapor", from: "4.70.0" ), .package( url: "https://github.com/vapor/fluent", from: "4.4.0" ), .package( url: "https://github.com/vapor/fluent-sqlite-driver", from: "4.1.0" ), .package( url: "https://github.com/binarybirds/liquid", from: "1.3.0" ), .package( url: "https://github.com/binarybirds/liquid-local-driver", from: "1.3.0" ), .package( url: "https://github.com/binarybirds/swift-html", from: "1.7.0" ), ], targets: [ .target(name: "App", dependencies: [ .product(name: "Vapor", package: "vapor"), .product(name: "Fluent", package: "fluent"), .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), .product(name: "Liquid", package: "liquid"), .product(name: "LiquidLocalDriver", package: "liquid-local-driver"), .product(name: "SwiftHtml", package: "swift-html"), .product(name: "SwiftSvg", package: "swift-html"), ]), .executableTarget(name: "Run", dependencies: ["App"]), .testTarget(name: "AppTests", dependencies: [ .target(name: "App"), .product(name: "XCTVapor", package: "vapor"), ]) ] ) ================================================ FILE: Chapter 11/myProject/Public/css/admin.css ================================================ tr { column-gap: 1rem; } ================================================ FILE: Chapter 11/myProject/Public/css/web.css ================================================ #blog h2 { margin: 0.5rem 0; } ================================================ FILE: Chapter 11/myProject/Public/js/admin.js ================================================ document.addEventListener("keydown", function(e) { if ( (window.navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey) && e.keyCode == 83 ) { e.preventDefault(); document.forms[0].submit(); } }, false); ================================================ FILE: Chapter 11/myProject/Public/js/web.js ================================================ function about() { alert("myPage\n\nversion 1.0.0"); } ================================================ FILE: Chapter 11/myProject/Sources/App/Extensions/Svg+MenuIcon.swift ================================================ import SwiftSvg extension Svg { static func menuIcon() -> Svg { Svg { Line(x1: 3, y1: 12, x2: 21, y2: 12) Line(x1: 3, y1: 6, x2: 21, y2: 6) Line(x1: 3, y1: 18, x2: 21, y2: 18) } .width(24) .height(24) .viewBox(minX: 0, minY: 0, width: 24, height: 24) .fill("none") .stroke("currentColor") .strokeWidth(2) .strokeLinecap("round") .strokeLinejoin("round") } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/AuthenticatedUser.swift ================================================ import Vapor public struct AuthenticatedUser { public let id: UUID public let email: String public init( id: UUID, email: String ) { self.id = id self.email = email } } extension AuthenticatedUser: SessionAuthenticatable { public var sessionID: UUID { id } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Controllers/ModelController.swift ================================================ import Vapor import Fluent public struct Name { let singular: String let plural: String init( singular: String, plural: String? = nil ) { self.singular = singular self.plural = plural ?? singular + "s" } } public protocol ModelController { associatedtype DatabaseModel: DatabaseModelInterface var modelName: Name { get } var parameterId: String { get } func identifier( _ req: Request ) throws -> UUID func findBy( _ id: UUID, on: Database ) async throws -> DatabaseModel } extension ModelController { func identifier(_ req: Request) throws -> UUID { guard let id = req.parameters.get(parameterId), let uuid = UUID(uuidString: id) else { throw Abort(.badRequest) } return uuid } func findBy( _ id: UUID, on db: Database ) async throws -> DatabaseModel { guard let model = try await DatabaseModel.find(id, on: db) else { throw Abort(.notFound) } return model } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/DatabaseModelInterface.swift ================================================ import Vapor import Fluent public protocol DatabaseModelInterface: Fluent.Model where Self.IDValue == UUID { associatedtype Module: ModuleInterface static var identifier: String { get } } public extension DatabaseModelInterface { static var schema: String { Module.identifier + "_" + identifier } static var identifier: String { String(describing: self) .dropFirst(Module.identifier.count) .dropLast(5) .lowercased() + "s" } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Extensions/ByteBuffer+Data.swift ================================================ import Vapor public extension ByteBuffer { var data: Data? { getData(at: 0, length: readableBytes) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Extensions/File+ByteBuffer.swift ================================================ import Vapor public extension File { var byteBuffer: ByteBuffer { data } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Form/AbstractForm.swift ================================================ import Vapor open class AbstractForm: FormComponent { open var action: FormAction open var fields: [FormComponent] open var error: String? open var submit: String? public init( action: FormAction = .init(), fields: [FormComponent] = [], error: String? = nil, submit: String? = nil ) { self.action = action self.fields = fields self.error = error self.submit = submit self.action.enctype = .multipart } open func load(req: Request) async throws { for field in fields { try await field.load(req: req) } } open func process(req: Request) async throws { for field in fields { try await field.process(req: req) } } open func validate(req: Request) async throws -> Bool { var result: [Bool] = [] for field in fields { result.append(try await field.validate(req: req)) } return result.filter { $0 == false }.isEmpty } open func write(req: Request) async throws { for field in fields { try await field.write(req: req) } } open func save(req: Request) async throws { for field in fields { try await field.save(req: req) } } open func read(req: Request) async throws { for field in fields { try await field.read(req: req) } } open func render(req: Request) -> TemplateRepresentable { FormTemplate(getContext(req)) } func getContext(_ req: Request) -> FormContext { .init( action: action, fields: fields.map { $0.render(req: req)}, error: error, submit: submit ) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Form/AbstractFormField.swift ================================================ import Vapor open class AbstractFormField< Input: Decodable, Output: TemplateRepresentable >: FormComponent { public var key: String public var input: Input public var output: Output public var error: String? // MARK: - event blocks public typealias FormFieldBlock = (Request, AbstractFormField) async throws -> Void public typealias FormFieldValidatorsBlock = ((Request, AbstractFormField) -> [AsyncValidator]) private var readBlock: FormFieldBlock? private var writeBlock: FormFieldBlock? private var loadBlock: FormFieldBlock? private var saveBlock: FormFieldBlock? private var validatorsBlock: FormFieldValidatorsBlock? // MARK: - init & config public init( key: String, input: Input, output: Output, error: String? = nil ) { self.key = key self.input = input self.output = output self.error = error } open func config( _ block: (AbstractFormField) -> Void ) -> Self { block(self) return self } // MARK: - Block setters open func read(_ block: @escaping FormFieldBlock) -> Self { readBlock = block return self } open func write(_ block: @escaping FormFieldBlock) -> Self { writeBlock = block return self } open func load(_ block: @escaping FormFieldBlock) -> Self { loadBlock = block return self } open func save(_ block: @escaping FormFieldBlock) -> Self { saveBlock = block return self } open func validators( @AsyncValidatorBuilder _ block: @escaping FormFieldValidatorsBlock ) -> Self { validatorsBlock = block return self } // MARK: - FormComponent open func process(req: Request) async throws { if let value = try? req.content.get(Input.self, at: key) { input = value } } open func validate(req: Request) async throws -> Bool { guard let validators = validatorsBlock else { return true } return await RequestValidator(validators(req, self)).isValid(req) } open func read(req: Request) async throws { try await readBlock?(req, self) } open func write(req: Request) async throws { try await writeBlock?(req, self) } open func load(req: Request) async throws { try await loadBlock?(req, self) } open func save(req: Request) async throws { try await saveBlock?(req, self) } open func render(req: Request) -> TemplateRepresentable { output } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Form/Fields/HiddenField.swift ================================================ import Vapor public final class HiddenField: AbstractFormField< String, HiddenFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Form/Fields/ImageField.swift ================================================ import Vapor public final class ImageField: AbstractFormField< FormImageInput, ImageFieldTemplate > { public var imageKey: String? { didSet { output.context.data.originalKey = imageKey } } public var path: String public init( _ key: String, path: String ) { self.path = path super.init( key: key, input: .init(key: key), output: .init(.init(key: key)) ) } public override func process( req: Request ) async throws { /// process input input.file = try? req.content.get( File.self, at: key ) input.data.originalKey = try? req.content.get( String.self, at: key + "OriginalKey" ) if let temporaryFileKey = try? req.content.get( String.self, at: key + "TemporaryFileKey" ), let temporaryFileName = try? req.content.get( String.self, at: key + "TemporaryFileName" ) { input.data.temporaryFile = .init( key: temporaryFileKey, name: temporaryFileName ) } input.data.shouldRemove = (try? req.content.get( Bool.self, at: key + "ShouldRemove" )) ?? false /// remove & upload file if input.data.shouldRemove { if let originalKey = input.data.originalKey { try? await req.fs.delete(key: originalKey) } } else if let file = input.file, let data = file.byteBuffer.data, !data.isEmpty { if let tmpKey = input.data.temporaryFile?.key { try? await req.fs.delete(key: tmpKey) } let key = "tmp/\(UUID().uuidString).tmp" _ = try await req.fs.upload(key: key, data: data) /// update the temporary image input.data.temporaryFile = .init( key: key, name: file.filename ) } /// update output values output.context.data = input.data } public override func write( req: Request ) async throws { imageKey = input.data.originalKey if input.data.shouldRemove { if let key = input.data.originalKey { try? await req.fs.delete(key: key) } imageKey = nil } else if let file = input.data.temporaryFile { var newKey = path + "/" + file.name if await req.fs.exists(key: newKey) { let formatter = DateFormatter() formatter.dateFormat="y-MM-dd-HH-mm-ss-" let prefix = formatter.string(from: .init()) newKey = path + "/" + prefix + file.name } _ = try await req.fs.move(key: file.key, to: newKey) input.data.temporaryFile = nil if let key = input.data.originalKey { try? await req.fs.delete(key: key) } imageKey = newKey } try await super.write(req: req) } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Form/Fields/InputField.swift ================================================ public final class InputField: AbstractFormField< String, InputFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init( key: key ) ) ) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Form/Fields/SelectField.swift ================================================ import Vapor public final class SelectField: AbstractFormField< String, SelectFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Form/Fields/TextareaField.swift ================================================ import Vapor public final class TextareaField: AbstractFormField< String, TextareaFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Form/FormAction.swift ================================================ import SwiftHtml public struct FormAction { public var method: SwiftHtml.Method public var url: String? public var enctype: SwiftHtml.Enctype? public init( method: SwiftHtml.Method = .post, url: String? = nil, enctype: SwiftHtml.Enctype? = nil ) { self.method = method self.url = url self.enctype = enctype } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Form/FormComponent.swift ================================================ import Vapor public protocol FormComponent { func load(req: Request) async throws func process(req: Request) async throws func validate(req: Request) async throws -> Bool func write(req: Request) async throws func save(req: Request) async throws func read(req: Request) async throws func render(req: Request) -> TemplateRepresentable } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Form/FormComponentBuilder.swift ================================================ @resultBuilder public enum FormComponentBuilder { public static func buildBlock( _ components: FormComponent... ) -> [FormComponent] { components } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Form/FormImageData.swift ================================================ public struct FormImageData: Codable { public struct TemporaryFile: Codable { public let key: String public let name: String public init( key: String, name: String ) { self.key = key self.name = name } } public var originalKey: String? public var temporaryFile: TemporaryFile? public var shouldRemove: Bool public init( originalKey: String? = nil, temporaryFile: TemporaryFile? = nil, shouldRemove: Bool = false ) { self.originalKey = originalKey self.temporaryFile = temporaryFile self.shouldRemove = shouldRemove } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Form/FormImageInput.swift ================================================ import Vapor public struct FormImageInput: Codable { public var key: String public var file: File? public var data: FormImageData public init( key: String, file: File? = nil, data: FormImageData? = nil ) { self.key = key self.file = file self.data = data ?? .init() } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/ModelEditorInterface.swift ================================================ import Vapor public protocol ModelEditorInterface: FormComponent { associatedtype Model: DatabaseModelInterface var model: Model { get } var form: AbstractForm { get } init(model: Model, form: AbstractForm) @FormComponentBuilder var formFields: [FormComponent] { get } } public extension ModelEditorInterface { func load(req: Request) async throws { try await form.load(req: req) } func process(req: Request) async throws { try await form.process(req: req) } func validate(req: Request) async throws -> Bool { try await form.validate(req: req) } func write(req: Request) async throws { try await form.write(req: req) } func save(req: Request) async throws { try await form.save(req: req) } func read(req: Request) async throws { try await form.read(req: req) } func render(req: Request) -> TemplateRepresentable { form.render(req: req) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/ModuleInterface.swift ================================================ import Vapor public protocol ModuleInterface { static var identifier: String { get } func boot(_ app: Application) throws } public extension ModuleInterface { func boot(_ app: Application) throws {} static var identifier: String { String(describing: self).dropLast(6).lowercased() } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Contexts/CellContext.swift ================================================ public struct CellContext { public enum `Type`: String { case text case image } public let value: String public let link: LinkContext? public let type: `Type` public init( _ value: String, link: LinkContext? = nil, type: `Type` = .text ) { self.type = type self.value = value self.link = link } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Contexts/ColumnContext.swift ================================================ public struct ColumnContext { public let key: String public let label: String public init( _ key: String, label: String? = nil ) { self.key = key self.label = label ?? key.capitalized } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Contexts/DetailContext.swift ================================================ public struct DetailContext { public enum `Type`: String { case text case image } public let key: String public let label: String public let value: String public let type: `Type` public init( _ key: String, _ value: String, label: String? = nil, type: `Type` = .text ) { self.key = key self.label = label ?? key.capitalized self.value = value self.type = type } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Contexts/FormContext.swift ================================================ public struct FormContext { public var action: FormAction public var fields: [TemplateRepresentable] public var error: String? public var submit: String? } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Contexts/HiddenFieldContext.swift ================================================ public struct HiddenFieldContext { public let key: String public var value: String? public init( key: String, value: String? = nil ) { self.key = key self.value = value } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Contexts/ImageFieldContext.swift ================================================ public struct ImageFieldContext { public let key: String public var label: LabelContext public var data: FormImageData public var previewUrl: String? public var accept: String? public var error: String? public init( key: String, label: LabelContext? = nil, data: FormImageData = .init(), previewUrl: String? = nil, accept: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.data = data self.previewUrl = previewUrl self.accept = accept self.error = error } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Contexts/InputFieldContext.swift ================================================ import SwiftHtml public struct InputFieldContext { public let key: String public var label: LabelContext public var type: SwiftHtml.Input.`Type` public var placeholder: String? public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, type: Input.`Type` = .text, placeholder: String? = nil, value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.type = type self.placeholder = placeholder self.value = value self.error = error } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Contexts/LabelContext.swift ================================================ public struct LabelContext { public var key: String public var title: String? public var required: Bool public var more: String? public init( key: String, title: String? = nil, required: Bool = false, more: String? = nil ) { self.key = key self.title = title self.required = required self.more = more } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Contexts/LinkContext.swift ================================================ import Vapor public struct LinkContext { public let label: String public let path: String public let absolute: Bool public let isBlank: Bool public let dropLast: Int public init( label: String, path: String = "", absolute: Bool = false, isBlank: Bool = false, dropLast: Int = 0 ) { self.label = label self.path = path self.absolute = absolute self.isBlank = isBlank self.dropLast = dropLast } public func url( _ req: Request, _ infix: [PathComponent] = [] ) -> String { if absolute { return path } return "/" + (req.url.path.pathComponents.dropLast(dropLast) + (infix + path.pathComponents)).string } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Contexts/OptionContext.swift ================================================ public struct OptionContext { public var key: String public var label: String public init( key: String, label: String ) { self.key = key self.label = label } } public extension OptionContext { static func yesNo() -> [OptionContext] { ["yes", "no"].map { .init(key: $0, label: $0.capitalized) } } static func trueFalse() -> [OptionContext] { [true, false].map { .init(key: String($0), label: String($0).capitalized) } } static func numbers( _ numbers: [Int] ) -> [OptionContext] { numbers.map { .init(key: String($0), label: String($0)) } } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Contexts/RowContext.swift ================================================ public struct RowContext { public let id: String public let cells: [CellContext] public init( id: String, cells: [CellContext] ) { self.id = id self.cells = cells } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Contexts/SelectFieldContext.swift ================================================ public struct SelectFieldContext { public let key: String public var label: LabelContext public var options: [OptionContext] public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, options: [OptionContext] = [], value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.options = options self.value = value self.error = error } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Contexts/TableContext.swift ================================================ public struct TableContext { public let columns: [ColumnContext] public let rows: [RowContext] public let actions: [LinkContext] public init( columns: [ColumnContext], rows: [RowContext], actions: [LinkContext] = [] ) { self.columns = columns self.rows = rows self.actions = actions } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Contexts/TextareaFieldContext.swift ================================================ public struct TextareaFieldContext { public let key: String public var label: LabelContext public var placeholder: String? public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, placeholder: String? = nil, value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.placeholder = placeholder self.value = value self.error = error } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Html/CellTemplate.swift ================================================ import Vapor import SwiftHtml public struct CellTemplate: TemplateRepresentable { var context: CellContext var rowId: String public init( _ context: CellContext, rowId: String ) { self.context = context self.rowId = rowId } @TagBuilder public func render( _ req: Request ) -> Tag { Td { switch context.type { case .text: if let link = context.link { LinkTemplate(link, pathInfix: rowId).render(req) } else { Text(context.value) } case .image: if let link = context.link { LinkTemplate(link, pathInfix: rowId) { label in Img(src: context.value, alt: label) } .render(req) } else { Img(src: context.value, alt: context.value) } } } .class("field") } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Html/DetailTemplate.swift ================================================ import Vapor import SwiftHtml public struct DetailTemplate: TemplateRepresentable { var context: DetailContext public init( _ context: DetailContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Dt(context.label) switch context.type { case .text: if context.value.isEmpty { Dd(" ") } else { Dd( context.value.replacingOccurrences( of: "\n", with: "
" ) ) } case .image: Dd { Img( src: context.value, alt: context.label ) } } } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Html/FormTemplate.swift ================================================ import Vapor import SwiftHtml public struct FormTemplate: TemplateRepresentable { var context: FormContext public init( _ context: FormContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Form { if let error = context.error { Section { P(error) .class("error") } } for field in context.fields { Section { field.render(req) } } Section { Input() .type(.submit) .value(context.submit ?? "Save") } } .method(context.action.method) .action(context.action.url) .enctype(context.action.enctype) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Html/HiddenFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct HiddenFieldTemplate: TemplateRepresentable { var context: HiddenFieldContext public init( _ context: HiddenFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Input() .type(.hidden) .name(context.key) .value(context.value) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Html/ImageFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct ImageFieldTemplate: TemplateRepresentable { public var context: ImageFieldContext public init( _ context: ImageFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { if let url = context.previewUrl { Img(src: req.fs.resolve(key: url), alt: context.key) } else { Img(src: "/img/logo.png", alt: "default post image") } LabelTemplate(context.label).render(req) Input() .type(.file) .key(context.key) .class("field") .accept(context.accept) if let temporaryFile = context.data.temporaryFile { Input() .key(context.key + "TemporaryFileKey") .value(temporaryFile.key) .type(.hidden) Input() .key(context.key + "TemporaryFileName") .value(temporaryFile.name) .type(.hidden) } if let key = context.data.originalKey { Input() .key(context.key + "OriginalKey") .value(key) .type(.hidden) } if !context.label.required { Input() .key(context.key + "ShouldRemove") .value(String(true)) .type(.checkbox) .checked(context.data.shouldRemove) Label("Remove") .for(context.key + "Remove") } if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Html/InputFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct InputFieldTemplate: TemplateRepresentable { public var context: InputFieldContext public init( _ context: InputFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Input() .type(context.type) .key(context.key) .placeholder(context.placeholder) .value(context.value) .class("field") if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Html/LabelTemplate.swift ================================================ import Vapor import SwiftHtml public struct LabelTemplate: TemplateRepresentable { var context: LabelContext public init( _ context: LabelContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Label { Text(context.title ?? context.key.capitalized) if let more = context.more { Span(more) .class("more") } if context.required { Span("*") .class("required") } }.for(context.key) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Html/LinkTemplate.swift ================================================ import Vapor import SwiftHtml public struct LinkTemplate: TemplateRepresentable { var context: LinkContext var body: Tag var pathInfix: String? public init( _ context: LinkContext, pathInfix: String? = nil, _ builder: ((String) -> Tag)? = nil ) { self.context = context self.pathInfix = pathInfix self.body = builder?(context.label) ?? Text(context.label) } @TagBuilder public func render( _ req: Request ) -> Tag { A { body } .href(context.url(req, pathInfix?.pathComponents ?? [])) .target(.blank, context.isBlank) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Html/SelectFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct SelectFieldTemplate: TemplateRepresentable { public var context: SelectFieldContext public init( _ context: SelectFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Select { for item in context.options { Option(item.label) .value(item.key) .selected(context.value == item.key) } } .name(context.key) if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Html/TableTemplate.swift ================================================ import Vapor import SwiftHtml public struct TableTemplate: TemplateRepresentable { var context: TableContext public init( _ context: TableContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Table { Thead { Tr { context.columns.map { column in Th(column.label) .id(column.key) .class("field") } context.actions.map { action in Th(action.label) .class("action") } } } Tbody { for row in context.rows { Tr { row.cells.map { CellTemplate($0, rowId: row.id).render(req) } context.actions.map { action in Td { LinkTemplate(action, pathInfix: row.id).render(req) } .class("action") } } .id(row.id) } } } } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Templates/Html/TextareaFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct TextareaFieldTemplate: TemplateRepresentable { public var context: TextareaFieldContext public init( _ context: TextareaFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Textarea(context.value) .placeholder(context.placeholder) .name(context.key) if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Validation/AsyncValidator.swift ================================================ import Vapor public protocol AsyncValidator { var key: String { get } var message: String { get } func validate( _ req: Request ) async throws -> ValidationErrorDetail? } public extension AsyncValidator { var error: ValidationErrorDetail { .init(key: key, message: message) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Validation/AsyncValidatorBuilder.swift ================================================ @resultBuilder public enum AsyncValidatorBuilder { public static func buildBlock( _ components: AsyncValidator... ) -> [AsyncValidator] { components } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Validation/FormFieldValidator+Validations.swift ================================================ import Vapor public extension FormFieldValidator where Input == String { static func required( _ field: AbstractFormField, _ message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is required" return .init(field, msg) { _, field in !field.input.isEmpty } } static func min( _ field: AbstractFormField, length: Int, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)" return .init(field, msg) { _, field in field.input.count >= length } } static func max( _ field: AbstractFormField, length: Int, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)" return .init(field, msg) { _, field in field.input.count <= length } } static func alphanumeric( _ field: AbstractFormField, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) should be only alphanumeric characters" return .init(field, msg) { _, field in !Validator .characterSet(.alphanumerics) .validate(field.input) .isFailure } } static func email( _ field: AbstractFormField, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) should be a valid email address" return .init(field, msg) { _, field in !Validator .email .validate(field.input) .isFailure } } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Validation/FormFieldValidator.swift ================================================ import Vapor public struct FormFieldValidator< Input: Decodable, Output: TemplateRepresentable >: AsyncValidator { public typealias AsyncValidationBlock = ((Request, AbstractFormField) async throws -> Bool) public let field: AbstractFormField public let message: String public let validation: AsyncValidationBlock public var key: String { field.key } public init( _ field: AbstractFormField, _ message: String, _ validation: @escaping AsyncValidationBlock ) { self.field = field self.message = message self.validation = validation } public func validate( _ req: Request ) async throws -> ValidationErrorDetail? { let isValid = try await validation(req, field) if isValid { return nil } field.error = message return error } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Validation/RequestValidator.swift ================================================ import Vapor public struct RequestValidator { public var validators: [AsyncValidator] public init( _ validators: [AsyncValidator] ) { self.validators = validators } public func validate( _ req: Request, message: String? = nil ) async throws { var result: [ValidationErrorDetail] = [] for validator in validators { if result.contains(where: { $0.key == validator.key }) { continue } if let res = try await validator.validate(req) { result.append(res) } } if !result.isEmpty { throw ValidationAbort( abort: Abort(.badRequest, reason: message), details: result ) } } public func isValid( _ req: Request ) async -> Bool { do { try await validate(req, message: nil) return true } catch { return false } } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Validation/ValidationAbort.swift ================================================ import Vapor public struct ValidationAbort: AbortError { public var abort: Abort public var message: String? public var details: [ValidationErrorDetail] public var reason: String { abort.reason } public var status: HTTPStatus { abort.status } public init( abort: Abort, message: String? = nil, details: [ValidationErrorDetail] ) { self.abort = abort self.message = message self.details = details } } ================================================ FILE: Chapter 11/myProject/Sources/App/Framework/Validation/ValidationErrorDetail.swift ================================================ import Vapor public struct ValidationErrorDetail: Codable { public var key: String public var message: String public init( key: String, message: String ) { self.key = key self.message = message } } extension ValidationErrorDetail: Content {} ================================================ FILE: Chapter 11/myProject/Sources/App/Middlewares/ExtendPathMiddleware.swift ================================================ import Vapor struct ExtendPathMiddleware: AsyncMiddleware { func respond( to req: Request, chainingTo next: AsyncResponder ) async throws -> Response { if !req.url.path.hasSuffix("/") && !req.url.path.contains(".") { return req.redirect( to: req.url.path + "/", redirectType: .permanent ) } return try await next.respond( to: req ) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Admin/AdminModule.swift ================================================ import Vapor struct AdminModule: ModuleInterface { let router = AdminRouter() func boot(_ app: Application) throws { try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Admin/AdminRouter.swift ================================================ import Vapor struct AdminRouter: RouteCollection { let controller = AdminFrontendController() func boot(routes: RoutesBuilder) throws { routes .grouped( AuthenticatedUser.redirectMiddleware( path: "/sign-in/" ) ) .get("admin", use: controller.dashboardView) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Admin/Controllers/AdminCreateController.swift ================================================ import Vapor protocol AdminCreateController: ModelController { associatedtype CreateModelEditor: ModelEditorInterface func createTemplate( _ req: Request, _ editor: CreateModelEditor ) -> TemplateRepresentable func createView( _ req: Request ) async throws -> Response func createAction( _ req: Request ) async throws -> Response func createContext( _ req: Request, _ editor: CreateModelEditor ) -> AdminEditorPageContext func createBreadcrumbs( _ req: Request ) -> [LinkContext] } extension AdminCreateController { private func render( _ req: Request, editor: CreateModelEditor ) -> Response { req.templates.renderHtml( createTemplate(req, editor) ) } func createView( _ req: Request ) async throws -> Response { let editor = CreateModelEditor( model: .init(), form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) return render(req, editor: editor) } func createAction( _ req: Request ) async throws -> Response { let model = DatabaseModel() let editor = CreateModelEditor( model: model as! CreateModelEditor.Model, form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) try await editor.process(req: req) let isValid = try await editor.validate(req: req) guard isValid else { return render(req, editor: editor) } try await editor.write(req: req) try await editor.model.create(on: req.db) try await editor.save(req: req) var components = req.url.path.pathComponents.dropLast() components += editor.model.id!.uuidString.pathComponents return req.redirect(to: "/" + components.string + "/update/") } func createTemplate( _ req: Request, _ editor: CreateModelEditor ) -> TemplateRepresentable { AdminEditorPageTemplate( createContext(req, editor) ) } func createContext( _ req: Request, _ editor: CreateModelEditor ) -> AdminEditorPageContext { let context = FormContext( action: editor.form.action, fields: editor.form.fields.map { $0.render(req: req) }, error: editor.form.error, submit: editor.form.submit ) return .init( title: "Create", form: context, breadcrumbs: createBreadcrumbs(req) ) } func createBreadcrumbs( _ req: Request ) -> [LinkContext] { [ LinkContext( label: DatabaseModel.Module.identifier.capitalized, dropLast: 2 ), LinkContext( label: modelName.plural.capitalized, dropLast: 1 ), ] } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Admin/Controllers/AdminDeleteController.swift ================================================ import Vapor final class DeleteForm: AbstractForm { init() { super.init() self.action.method = .post self.submit = "Delete" } } protocol AdminDeleteController: ModelController { func deleteView( _ req: Request ) async throws -> Response func deleteAction( _ req: Request ) async throws -> Response func deleteTemplate( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> TemplateRepresentable func deleteInfo( _ model: DatabaseModel ) -> String func deleteContext( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> AdminDeletePageContext } extension AdminDeleteController { func deleteView( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let form = DeleteForm() return req.templates.renderHtml( deleteTemplate(req, model, form) ) } func deleteAction( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) try await model.delete(on: req.db) var url = req.url.path if let redirect = try? req.query.get(String.self, at: "redirect") { url = redirect } return req.redirect(to: url) } func deleteTemplate( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> TemplateRepresentable { AdminDeletePageTemplate( deleteContext(req, model, form) ) } func deleteContext( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> AdminDeletePageContext { .init( title: "Delete", name: deleteInfo(model), type: "model", form: form.getContext(req) ) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Admin/Controllers/AdminDetailController.swift ================================================ import Vapor protocol AdminDetailController: ModelController { func detailView( _ req: Request ) async throws -> Response func detailTemplate( _ req: Request, _ model: DatabaseModel ) -> TemplateRepresentable func detailFields( for model: DatabaseModel ) -> [DetailContext] func detailContext( _ req: Request, _ model: DatabaseModel ) -> AdminDetailPageContext func detailBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func detailNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] } extension AdminDetailController { func detailView( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) return req.templates.renderHtml(detailTemplate(req, model)) } func detailTemplate( _ req: Request, _ model: DatabaseModel ) -> TemplateRepresentable { AdminDetailPageTemplate(detailContext(req, model)) } func detailContext( _ req: Request, _ model: DatabaseModel ) -> AdminDetailPageContext { let path = "/delete/?redirect=" + req.url.path.pathComponents.dropLast().string + "&cancel=" + req.url.path return .init( title: "Details", fields: detailFields(for: model), navigation: detailNavigation(req, model), breadcrumbs: detailBreadcrumbs(req, model), actions: [ LinkContext( label: "Delete", path: path ), ] ) } func detailBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: DatabaseModel.Module.identifier.capitalized, dropLast: 2 ), LinkContext( label: modelName.plural.capitalized, dropLast: 1 ), ] } func detailNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: "Update", path: "update" ), ] } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Admin/Controllers/AdminFrontendController.swift ================================================ import Vapor struct AdminFrontendController { func dashboardView( req: Request ) throws -> Response { let user = try req.auth.require(AuthenticatedUser.self) let template = AdminDashboardTemplate( .init( icon: "👋", title: "Dashboard", message: "Hello \(user.email), welcome to the CMS." ) ) return req.templates.renderHtml(template) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Admin/Controllers/AdminListController.swift ================================================ import Vapor protocol AdminListController: ModelController { func list(_ req: Request) async throws -> [DatabaseModel] func listView(_ req: Request) async throws -> Response func listColumns() -> [ColumnContext] func listCells(for model: DatabaseModel) -> [CellContext] func listNavigation(_ req: Request) -> [LinkContext] func listBreadcrumbs(_ req: Request) -> [LinkContext] func listContext( _ req: Request, _ list: [DatabaseModel] ) -> AdminListPageContext func listTemplate( _ req: Request, _ list: [DatabaseModel] ) -> TemplateRepresentable } extension AdminListController { func list(_ req: Request) async throws -> [DatabaseModel] { try await DatabaseModel.query(on: req.db).all() } func listView(_ req: Request) async throws -> Response { let list = try await list(req) let template = listTemplate(req, list) return req.templates.renderHtml(template) } func listNavigation(_ req: Request) -> [LinkContext] { [ LinkContext(label: "Create",path: "create") ] } func listBreadcrumbs(_ req: Request) -> [LinkContext] { [ LinkContext( label: DatabaseModel.Module.identifier.capitalized, dropLast: 1 ) ] } func listContext( _ req: Request, _ list: [DatabaseModel] ) -> AdminListPageContext { let rows = list.map { RowContext(id: $0.id!.uuidString, cells: listCells(for: $0)) } let table = TableContext(columns: listColumns(), rows: rows, actions: [ LinkContext(label: "Update", path: "update"), LinkContext(label: "Delete", path: "delete") ]) return .init( title: "List", table: table, navigation: listNavigation(req), breadcrumbs: listBreadcrumbs(req) ) } func listTemplate( _ req: Request, _ list: [DatabaseModel] ) -> TemplateRepresentable { AdminListPageTemplate(listContext(req, list)) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Admin/Controllers/AdminUpdateController.swift ================================================ import Vapor protocol AdminUpdateController: ModelController { associatedtype UpdateModelEditor: ModelEditorInterface func updateView( _ req: Request ) async throws -> Response func updateAction( _ req: Request ) async throws -> Response func updateTemplate( _ req: Request, _ editor: UpdateModelEditor ) async -> TemplateRepresentable func updateContext( _ req: Request, _ editor: UpdateModelEditor ) async -> AdminEditorPageContext func updateBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func updateNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] } extension AdminUpdateController { private func render( _ req: Request, editor: UpdateModelEditor ) async -> Response { req.templates.renderHtml( await updateTemplate(req, editor) ) } func updateView( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let editor = UpdateModelEditor( model: model as! UpdateModelEditor.Model, form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) try await editor.read(req: req) return await render(req, editor: editor) } func updateAction( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let editor = UpdateModelEditor( model: model as! UpdateModelEditor.Model, form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) try await editor.process(req: req) let isValid = try await editor.validate(req: req) guard isValid else { return await render(req, editor: editor) } try await editor.write(req: req) try await editor.model.update(on: req.db) try await editor.save(req: req) return req.redirect(to: req.url.path) } func updateTemplate( _ req: Request, _ editor: UpdateModelEditor ) async -> TemplateRepresentable { await AdminEditorPageTemplate( updateContext(req, editor) ) } func updateContext( _ req: Request, _ editor: UpdateModelEditor ) async -> AdminEditorPageContext { let path = "delete/?redirect=" + req.url.path.pathComponents.dropLast(2).string + "&cancel=" + req.url.path let model = editor.model as! DatabaseModel return .init( title: "Update", form: editor.form.getContext(req), navigation: updateNavigation(req, model), breadcrumbs: updateBreadcrumbs(req, model), actions: [ LinkContext( label: "Delete", path: path, dropLast: 1 ), ] ) } func updateBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: DatabaseModel.Module.identifier.capitalized, dropLast: 3 ), LinkContext( label: modelName.plural.capitalized, dropLast: 2 ), ] } func updateNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: "Details", dropLast: 1 ), ] } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDashboardContext.swift ================================================ struct AdminDashboardContext { let icon: String let title: String let message: String } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDeletePageContext.swift ================================================ public struct AdminDeletePageContext { public let title: String public let name: String public let type: String public let form: FormContext public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public init( title: String, name: String, type: String, form: FormContext, navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [] ) { self.title = title self.name = name self.type = type self.form = form self.navigation = navigation self.breadcrumbs = breadcrumbs } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDetailPageContext.swift ================================================ public struct AdminDetailPageContext { public let title: String public let fields: [DetailContext] public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public let actions: [LinkContext] public init( title: String, fields: [DetailContext], navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [], actions: [LinkContext] = [] ) { self.title = title self.fields = fields self.navigation = navigation self.breadcrumbs = breadcrumbs self.actions = actions } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminEditorPageContext.swift ================================================ public struct AdminEditorPageContext { public let title: String public let form: FormContext public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public let actions: [LinkContext] public init( title: String, form: FormContext, navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [], actions: [LinkContext] = [] ) { self.title = title self.form = form self.navigation = navigation self.breadcrumbs = breadcrumbs self.actions = actions } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminIndexContext.swift ================================================ public struct AdminIndexContext { public let title: String public let breadcrumbs: [LinkContext] public init( title: String, breadcrumbs: [LinkContext] = [] ) { self.title = title self.breadcrumbs = breadcrumbs } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminListPageContext.swift ================================================ public struct AdminListPageContext { public let title: String public let table: TableContext public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public init( title: String, table: TableContext, navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [] ) { self.title = title self.table = table self.navigation = navigation self.breadcrumbs = breadcrumbs } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDashboardTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminDashboardTemplate: TemplateRepresentable { var context: AdminDashboardContext init( _ context: AdminDashboardContext ) { self.context = context } @TagBuilder func render(_ req: Request) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } Nav { H2("Blog") Ul { Li { A("Posts") .href("/admin/blog/posts/") } Li { A("Categories") .href("/admin/blog/categories/") } } } } .id("dashboard") .class("container") } .render(req) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDeletePageTemplate.swift ================================================ import Vapor import SwiftHtml public struct AdminDeletePageTemplate: TemplateRepresentable { var context: AdminDeletePageContext public init( _ context: AdminDeletePageContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { Span("🗑") .class("icon") H1(context.title) P("You are about to permanently delete the
`\(context.name)` \(context.type).") FormTemplate(context.form).render(req) A("Cancel") .href((try? req.query.get(String.self, at: "cancel")) ?? "#") .class(["button", "cancel"]) } .class(["lead", "container", "center"]) } .render(req) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDetailPageTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminDetailPageTemplate: TemplateRepresentable { var context: AdminDetailPageContext init( _ context: AdminDetailPageContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { Div { H1(context.title) for item in context.navigation { LinkTemplate(item).render(req) } } .class("lead") Dl { for item in context.fields { DetailTemplate(item).render(req) } } Section { for item in context.actions { LinkTemplate(item).render(req) } } } .class("container") } .render(req) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Admin/Templates/Html/AdminEditorPageTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminEditorPageTemplate: TemplateRepresentable { var context: AdminEditorPageContext init( _ context: AdminEditorPageContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { Div { H1(context.title) for item in context.navigation { LinkTemplate(item).render(req) } } .class("lead") FormTemplate(context.form).render(req) Section { for item in context.actions { LinkTemplate(item).render(req) } } } .class("container") } .render(req) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Admin/Templates/Html/AdminIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg public struct AdminIndexTemplate: TemplateRepresentable { public var context: AdminIndexContext var body: Tag public init( _ context: AdminIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Meta() .name("robots") .content("noindex") Link(rel: .shortcutIcon) .href("/images/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css") Link(rel: .stylesheet) .href("/css/admin.css") Title(context.title) } Body { Div { A { Img(src: "/img/logo.png", alt: "Logo") .title("Logo") .style("width: 300px") } .href("/") Nav { Input() .type(.checkbox) .id("secondary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("secondary-menu-button") Div { A("Sign out") .href("/sign-out/") } .class("menu-items") } .id("secondary-menu") } .id("navigation") Div { Nav { A("Admin") .href("/admin/") for breadcrumb in context.breadcrumbs { LinkTemplate(breadcrumb).render(req) } } } .class("breadcrumb") Main { body } Script() .type(.javascript) .src("/js/admin.js") } } .lang("en-US") } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Admin/Templates/Html/AdminListPageTemplate.swift ================================================ import Vapor import SwiftHtml public struct AdminListPageTemplate: TemplateRepresentable { var context: AdminListPageContext public init( _ context: AdminListPageContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { H1(context.title) P { context.navigation.map { LinkTemplate($0).render(req) } } } .class("lead") if context.table.rows.isEmpty { Div { Span("🔍") .class("icon") H2("Oh no") P("This list is empty right now.") A("Try again →") .href(req.url.path) .class("button-1") } .class(["lead", "container", "center"]) } else { TableTemplate(context.table).render(req) } } .render(req) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/BlogModule.swift ================================================ import Vapor struct BlogModule: ModuleInterface { let router = BlogRouter() func boot(_ app: Application) throws { app.migrations.add(BlogMigrations.v1()) app.migrations.add(BlogMigrations.seed()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/BlogRouter.swift ================================================ import Vapor struct BlogRouter: RouteCollection { let controller = BlogFrontendController() let postAdminController = BlogPostAdminController() let categoryAdminController = BlogCategoryAdminController() let categoryApiController = BlogCategoryApiController() func boot(routes: RoutesBuilder) throws { routes.get("blog", use: controller.blogView) routes.get(.anything, use: controller.postView) let blog = routes .grouped(AuthenticatedUser.redirectMiddleware(path: "/")) .grouped("admin", "blog") let categories = blog.grouped("categories") categories.get(use: categoryAdminController.listView) categories.get("create", use: categoryAdminController.createView) categories.post("create", use: categoryAdminController.createAction) let categoryId = categories.grouped(":categoryId") categoryId.get(use: categoryAdminController.detailView) categoryId.get("update", use: categoryAdminController.updateView) categoryId.post("update", use: categoryAdminController.updateAction) categoryId.get("delete", use: categoryAdminController.deleteView) categoryId.post("delete", use: categoryAdminController.deleteAction) let posts = blog.grouped("posts") posts.get(use: postAdminController.listView) let postId = posts.grouped(":postId") postId.get(use: postAdminController.detailView) posts.get("create", use: postAdminController.createView) posts.post("create", use: postAdminController.createAction) postId.get("update", use: postAdminController.updateView) postId.post("update", use: postAdminController.updateAction) postId.get("delete", use: postAdminController.deleteView) postId.post("delete", use: postAdminController.deleteAction) let blogApi = routes.grouped("api", "blog") let categoriesApi = blogApi.grouped("categories") categoriesApi.get(use: categoryApiController.listApi) let categoryApiId = categoriesApi.grouped(":categoryId") categoryApiId.get(use: categoryApiController.detailApi) categoriesApi.post(use: categoryApiController.createApi) categoryApiId.put(use: categoryApiController.updateApi) categoryApiId.patch(use: categoryApiController.patchApi) categoryApiId.delete(use: categoryApiController.deleteApi) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/Controllers/BlogCategoryAdminController.swift ================================================ import Vapor import Fluent struct BlogCategoryAdminController: AdminListController, AdminDetailController, AdminCreateController, AdminUpdateController, AdminDeleteController { typealias DatabaseModel = BlogCategoryModel typealias CreateModelEditor = BlogCategoryEditor typealias UpdateModelEditor = BlogCategoryEditor let modelName: Name = .init( singular: "category", plural: "categories" ) let parameterId: String = "categoryId" func listColumns() -> [ColumnContext] { [ .init("title"), ] } func listCells( for model: DatabaseModel ) -> [CellContext] { [ .init( model.title, link: .init(label: model.title) ), ] } func detailFields( for model: DatabaseModel ) -> [DetailContext] { [ .init("title", model.title), ] } func deleteInfo( _ model: DatabaseModel ) -> String { model.title } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/Controllers/BlogCategoryApiController.swift ================================================ import Vapor extension Blog.Category.List: Content {} extension Blog.Category.Detail: Content {} struct BlogCategoryApiController: ModelController { typealias DatabaseModel = BlogCategoryModel var modelName: Name = .init( singular: "category", plural: "categories" ) var parameterId: String = "categoryId" func listOutput( _ req: Request, _ models: [BlogCategoryModel] ) async throws -> [Blog.Category.List] { models.map { .init(id: $0.id!, title: $0.title) } } func listApi( _ req: Request ) async throws -> [Blog.Category.List] { let models = try await BlogCategoryModel .query(on: req.db) .all() return try await listOutput(req, models) } func detailOutput( _ req: Request, _ model: BlogCategoryModel ) async throws -> Blog.Category.Detail { .init( id: model.id!, title: model.title ) } func detailApi( _ req: Request ) async throws -> Blog.Category.Detail { let model = try await findBy(identifier(req), on: req.db) return try await detailOutput(req, model) } func createInput( _ req: Request, _ model: BlogCategoryModel, _ input: Blog.Category.Create ) async throws { model.title = input.title } func createApi( _ req: Request ) async throws -> Response { let input = try req.content.decode(Blog.Category.Create.self) let model = DatabaseModel() try await createInput(req, model, input) try await model.create(on: req.db) return try await detailOutput(req, model) .encodeResponse(status: .created, for: req) } func updateInput( _ req: Request, _ model: BlogCategoryModel, _ input: Blog.Category.Update ) async throws { model.title = input.title } func updateApi( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let input = try req.content.decode(Blog.Category.Update.self) try await updateInput(req, model, input) try await model.update(on: req.db) return try await detailOutput(req, model) .encodeResponse(for: req) } func patchInput( _ req: Request, _ model: BlogCategoryModel, _ input: Blog.Category.Patch ) async throws { model.title = input.title ?? model.title } func patchApi( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let input = try req.content.decode(Blog.Category.Patch.self) try await patchInput(req, model, input) try await model.update(on: req.db) return try await detailOutput(req, model) .encodeResponse(for: req) } func deleteApi( _ req: Request ) async throws -> HTTPStatus { let model = try await findBy(identifier(req), on: req.db) try await model.delete(on: req.db) return .noContent } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift ================================================ import Vapor import Fluent struct BlogFrontendController { func blogView(req: Request) async throws -> Response { let posts = try await BlogPostModel .query(on: req.db) .sort(\.$date, .descending) .all() let api = BlogPostApiController() let list = posts.map { api.mapList($0) } let ctx = BlogPostsContext( icon: "🔥", title: "Blog", message: "Hot news and stories about everything.", posts: list ) return req.templates.renderHtml(BlogPostsTemplate(ctx)) } func postView(req: Request) async throws -> Response { let slug = req.url.path.trimmingCharacters( in: .init(charactersIn: "/") ) guard let post = try await BlogPostModel .query(on: req.db) .filter(\.$slug == slug) .with(\.$category) .first() else { return req.redirect(to: "/") } let api = BlogPostApiController() let ctx = BlogPostContext(post: api.mapDetail(post)) return req.templates.renderHtml(BlogPostTemplate(ctx)) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/Controllers/BlogPostAdminController.swift ================================================ import Vapor import Fluent struct BlogPostAdminController: AdminListController, AdminDetailController, AdminCreateController, AdminUpdateController, AdminDeleteController { typealias DatabaseModel = BlogPostModel typealias CreateModelEditor = BlogPostEditor typealias UpdateModelEditor = BlogPostEditor let modelName: Name = .init(singular: "post") let parameterId: String = "postId" func listColumns() -> [ColumnContext] { [ .init("image"), .init("title"), ] } func listCells(for model: DatabaseModel) -> [CellContext] { [ .init(model.imageKey, type: .image), .init(model.title, link: .init(label: model.title)), ] } func detailFields(for model: DatabaseModel) -> [DetailContext] { [ .init("image", model.imageKey, type: .image), .init("title", model.title), ] } func deleteInfo( _ model: DatabaseModel ) -> String { model.title } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/Controllers/BlogPostApiController.swift ================================================ import Vapor struct BlogPostApiController { func mapList(_ model: BlogPostModel) -> Blog.Post.List { .init( id: model.id!, title: model.title, slug: model.slug, image: model.imageKey, excerpt: model.excerpt, date: model.date ) } func mapDetail(_ model: BlogPostModel) -> Blog.Post.Detail { .init( id: model.id!, title: model.title, slug: model.slug, image: model.imageKey, excerpt: model.excerpt, date: model.date, category: .init( id: model.category.id!, title: model.category.title ), content: model.content ) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/Database/Migrations/BlogMigrations.swift ================================================ import Foundation import Fluent enum BlogMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema) .id() .field(BlogCategoryModel.FieldKeys.v1.title, .string, .required) .create() try await db.schema(BlogPostModel.schema) .id() .field(BlogPostModel.FieldKeys.v1.title, .string, .required) .field(BlogPostModel.FieldKeys.v1.slug, .string, .required) .field(BlogPostModel.FieldKeys.v1.imageKey, .string, .required) .field(BlogPostModel.FieldKeys.v1.excerpt, .data, .required) .field(BlogPostModel.FieldKeys.v1.date, .datetime, .required) .field(BlogPostModel.FieldKeys.v1.content, .data, .required) .field(BlogPostModel.FieldKeys.v1.categoryId, .uuid) .foreignKey( BlogPostModel.FieldKeys.v1.categoryId, references: BlogCategoryModel.schema, .id, onDelete: DatabaseSchema.ForeignKeyAction.setNull, onUpdate: .cascade ) .unique(on: BlogPostModel.FieldKeys.v1.slug) .create() } func revert(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema).delete() try await db.schema(BlogPostModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let categories = (1...4).map { index in BlogCategoryModel(title: "Sample category #\(index)") } try await categories.create(on: db) try await (1...9).map { index in BlogPostModel( id: nil, title: "Sample post #\(index)", slug: "sample-post-\(index)", imageKey: "/img/posts/\(String(format: "%02d", index + 1)).jpg", excerpt: "Lorem ipsum", date: Date().addingTimeInterval(-Double.random(in: 0...(86400 * 60))), content: "Lorem ipsum dolor sit amet.", categoryId: categories[Int.random(in: 0.. [FormComponent] { ImageField("image", path: "blog/post") .read { [unowned self] in $1.output.context.previewUrl = model.imageKey ($1 as! ImageField).imageKey = model.imageKey } .write { [unowned self] in model.imageKey = ($1 as! ImageField).imageKey ?? "" } InputField("slug") .config { $0.output.context.label.required = true } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = model.slug } .write { [unowned self] in model.slug = $1.input } InputField("title") .config { $0.output.context.label.required = true } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = model.title } .write { [unowned self] in model.title = $1.input } InputField("date") .config { $0.output.context.label.required = true $0.output.context.value = dateFormatter.string(from: Date()) } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = dateFormatter.string(from: model.date) } .write { [unowned self] in model.date = dateFormatter.date(from: $1.input) ?? Date() } TextareaField("excerpt") .read { [unowned self] in $1.output.context.value = model.excerpt } .write { [unowned self] in model.excerpt = $1.input } TextareaField("content") .read { [unowned self] in $1.output.context.value = model.content } .write { [unowned self] in model.content = $1.input } SelectField("category") .load { req, field in let categories = try await BlogCategoryModel .query(on: req.db) .all() field.output.context.options = categories.map { OptionContext(key: $0.id!.uuidString, label: $0.title) } } .read { [unowned self] req, field in field.output.context.value = model.$category.id.uuidString } .write { [unowned self] req, field in if let uuid = UUID(uuidString: field.input), let category = try await BlogCategoryModel .find(uuid, on: req.db) { model.$category.id = category.id! } } } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/Objects/Blog.swift ================================================ enum Blog { enum Post { } enum Category { } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/Objects/BlogCategory.swift ================================================ import Foundation extension Blog.Category { struct List: Codable { let id: UUID let title: String } struct Detail: Codable { let id: UUID let title: String } struct Create: Codable { let title: String } struct Update: Codable { let title: String } struct Patch: Codable { let title: String? } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/Objects/BlogPost.swift ================================================ import Foundation extension Blog.Post { struct List: Codable { let id: UUID let title: String let slug: String let image: String let excerpt: String let date: Date } struct Detail: Codable { let id: UUID let title: String let slug: String let image: String let excerpt: String let date: Date let category: Blog.Category.List let content: String } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminDeleteContext.swift ================================================ struct BlogPostAdminDeleteContext { let title: String let name: String let type: String } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminDetailContext.swift ================================================ struct BlogPostAdminDetailContext { let title: String let detail: Blog.Post.Detail } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminEditContext.swift ================================================ struct BlogPostAdminEditContext { let title: String let form: TemplateRepresentable } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminListContext.swift ================================================ struct BlogPostAdminListContext { let title: String let list: [Blog.Post.List] } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostContext.swift ================================================ struct BlogPostContext { let post: Blog.Post.Detail } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostsContext.swift ================================================ struct BlogPostsContext { let icon: String let title: String let message: String let posts: [Blog.Post.List] } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminDeleteTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminDeleteTemplate: TemplateRepresentable { var context: BlogPostAdminDeleteContext init( _ context: BlogPostAdminDeleteContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Span("🗑") .class("icon") H1(context.title) P("You are about to permanently delete the
`\(context.name)` \(context.type).") Form { Input() .type(.submit) .class(["button", "destructive"]) .style("display: inline") .value("Delete") A("Cancel") .href("/admin/blog/posts/") .class(["button", "cancel"]) } .method(.post) .id("delete-form") } .class(["lead", "container", "center"]) } .render(req) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminDetailTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminDetailTemplate: TemplateRepresentable { var context: BlogPostAdminDetailContext init( _ context: BlogPostAdminDetailContext ) { self.context = context } var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .short return formatter }() @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") Dl { Dt("Image") Dd { Img( src: context.detail.image, alt: context.detail.title ) } Dt("Title") Dd(context.detail.title) Dt("Excerpt") Dd(context.detail.excerpt) Dt("Date") Dd(dateFormatter.string(from: context.detail.date)) Dt("Content") Dd(context.detail.content) } } .id("detail") .class("container") } .render(req) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminEditTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminEditTemplate: TemplateRepresentable { var context: BlogPostAdminEditContext init( _ context: BlogPostAdminEditContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") context.form.render(req) } .id("edit") .class("container") } .render(req) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminListTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminListTemplate: TemplateRepresentable { var context: BlogPostAdminListContext init( _ context: BlogPostAdminListContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") Table { Thead { Tr { Th("Image") Th("Title") Th("Preview") } } Tbody { for item in context.list { Tr { Td { Img(src: item.image, alt: item.title) } Td { A(item.title) .href("/admin/blog/posts/" + item.id.uuidString + "/") } Td { A("Preview") .href("/" + item.slug + "/") } } } } } } .id("list") } .render(req) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostTemplate: TemplateRepresentable { var context: BlogPostContext init( _ context: BlogPostContext ) { self.context = context } var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .short return formatter }() @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.post.title) ) { Div { Section { P(dateFormatter.string(from: context.post.date)) H1(context.post.title) P(context.post.excerpt) } .class(["lead", "container"]) Img(src: context.post.image, alt: context.post.title) Article { Text(context.post.content) } .class("container") } .id("post") } .render(req) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostsTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostsTemplate: TemplateRepresentable { var context: BlogPostsContext init( _ context: BlogPostsContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") Div { for post in context.posts { Article { A { Img(src: post.image, alt: post.title) H2(post.title) P(post.excerpt) } .href("/\(post.slug)/") } } } .class("grid-221") } .id("blog") } .render(req) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/User/Authenticators/UserCredentialsAuthenticator.swift ================================================ import Vapor import Fluent struct UserCredentialsAuthenticator: AsyncCredentialsAuthenticator { struct Credentials: Content { let email: String let password: String } func authenticate( credentials: Credentials, for req: Request ) async throws { guard let user = try await UserAccountModel .query(on: req.db) .filter(\.$email == credentials.email) .first() else { return } do { guard try Bcrypt.verify( credentials.password, created: user.password ) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } catch { // do nothing... } } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/User/Authenticators/UserSessionAuthenticator.swift ================================================ import Vapor import Fluent struct UserSessionAuthenticator: AsyncSessionAuthenticator { typealias User = AuthenticatedUser func authenticate( sessionID: User.SessionID, for req: Request ) async throws { guard let user = try await UserAccountModel .find(sessionID, on: req.db) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/User/Controllers/UserFrontendController.swift ================================================ import Vapor struct UserFrontendController { private func renderSignInView( _ req: Request, _ form: UserLoginForm ) -> Response { let template = UserLoginTemplate( .init( icon: "⬇️", title: "Sign in", message: "Please log in with your existing account", form: form.render(req: req) ) ) return req.templates.renderHtml(template) } func signInView( _ req: Request ) async throws -> Response { renderSignInView(req, .init()) } func signInAction( _ req: Request ) async throws -> Response { /// the user is authenticated, we can store the user data inside the session too if let user = req.auth.get(AuthenticatedUser.self) { req.session.authenticate(user) return req.redirect(to: "/") } let form = UserLoginForm() try await form.process(req: req) let isValid = try await form.validate(req: req) if !isValid { form.error = "Invalid email or password." } return renderSignInView(req, form) } func signOut( _ req: Request ) throws -> Response { req.auth.logout(AuthenticatedUser.self) req.session.unauthenticate(AuthenticatedUser.self) return req.redirect(to: "/") } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/User/Database/Migrations/UserMigrations.swift ================================================ import Vapor import Fluent enum UserMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(UserAccountModel.schema) .id() .field(UserAccountModel.FieldKeys.v1.email, .string, .required) .field(UserAccountModel.FieldKeys.v1.password, .string, .required) .unique(on: UserAccountModel.FieldKeys.v1.email) .create() } func revert(on db: Database) async throws { try await db.schema(UserAccountModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let email = "root@localhost.com" let password = "ChangeMe1" let user = UserAccountModel( email: email, password: try Bcrypt.hash(password) ) try await user.create(on: db) } func revert(on db: Database) async throws { try await UserAccountModel.query(on: db).delete() } } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/User/Database/Models/UserAccountModel.swift ================================================ import Vapor import Fluent final class UserAccountModel: DatabaseModelInterface { typealias Module = UserModule struct FieldKeys { struct v1 { static var email: FieldKey { "email" } static var password: FieldKey { "password" } } } @ID() var id: UUID? @Field(key: FieldKeys.v1.email) var email: String @Field(key: FieldKeys.v1.password) var password: String init() { } init( id: UUID? = nil, email: String, password: String ) { self.id = id self.email = email self.password = password } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/User/Forms/UserLoginForm.swift ================================================ import Vapor final class UserLoginForm: AbstractForm { public convenience init() { self.init( action: .init( method: .post, url: "/sign-in/" ), submit: "Sign in" ) self.fields = createFields() } @FormComponentBuilder func createFields() -> [FormComponent] { InputField("email") .config { $0.output.context.label.required = true $0.output.context.type = .email } .validators { FormFieldValidator.required($1) FormFieldValidator.email($1) } InputField("password") .config { $0.output.context.label.required = true $0.output.context.type = .password } .validators { FormFieldValidator.required($1) } } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/User/Templates/Contexts/UserLoginContext.swift ================================================ struct UserLoginContext { let icon: String let title: String let message: String let form: TemplateRepresentable init( icon: String, title: String, message: String, form: TemplateRepresentable ) { self.icon = icon self.title = title self.message = message self.form = form } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/User/Templates/Html/UserLoginTemplate.swift ================================================ import Vapor import SwiftHtml struct UserLoginTemplate: TemplateRepresentable { var context: UserLoginContext init( _ context: UserLoginContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") context.form.render(req) } .id("user-login") .class("container") } .render(req) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/User/UserModule.swift ================================================ import Vapor struct UserModule: ModuleInterface { let router = UserRouter() func boot(_ app: Application) throws { app.migrations.add(UserMigrations.v1()) app.migrations.add(UserMigrations.seed()) app.middleware.use(UserSessionAuthenticator()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/User/UserRouter.swift ================================================ import Vapor struct UserRouter: RouteCollection { let frontendController = UserFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get("sign-in", use: frontendController.signInView) routes .grouped( UserCredentialsAuthenticator() ) .post("sign-in", use: frontendController.signInAction) routes.get("sign-out", use: frontendController.signOut) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Web/Controllers/WebFrontendController.swift ================================================ import Vapor struct WebFrontendController { func homeView(req: Request) throws -> Response { let ctx = WebHomeContext( icon: "👋", title: "Home", message: "Hi there, welcome to my page.", paragraphs: [ "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.", "Nisi ut aliquip ex ea commodo consequat.", ], link: .init( label: "Read my blog →", url: "/blog/" ) ) return req.templates.renderHtml( WebHomeTemplate(ctx) ) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Web/Templates/Contexts/WebHomeContext.swift ================================================ struct WebHomeContext { let icon: String let title: String let message: String let paragraphs: [String] let link: WebLinkContext } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Web/Templates/Contexts/WebIndexContext.swift ================================================ public struct WebIndexContext { public let title: String public init( title: String ) { self.title = title } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Web/Templates/Contexts/WebLinkContext.swift ================================================ public struct WebLinkContext { public let label: String public let url: String public init( label: String, url: String ) { self.label = label self.url = url } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Web/Templates/Html/WebHomeTemplate.swift ================================================ import Vapor import SwiftHtml struct WebHomeTemplate: TemplateRepresentable { var context: WebHomeContext init( _ context: WebHomeContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") for paragraph in context.paragraphs { P(paragraph) } WebLinkTemplate(context.link).render(req) } .id("home") .class("container") } .render(req) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg public struct WebIndexTemplate: TemplateRepresentable { public var context: WebIndexContext var body: Tag public init( _ context: WebIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Link(rel: .shortcutIcon) .href("/images/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css") Link(rel: .stylesheet) .href("/css/web.css") Title(context.title) } Body { Header { Div { A { Img(src: "/img/logo.png", alt: "Logo") } .id("site-logo") .href("/") Nav { Input() .type(.checkbox) .id("primary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("primary-menu-button") Div { A("Home") .href("/") .class("selected", req.url.path == "/") A("Blog") .href("/blog/") .class("selected", req.url.path == "/blog/") A("About") .href("#") .onClick("javascript:about();") if req.auth.has(AuthenticatedUser.self) { A("Admin") .href("/admin/") A("Sign out") .href("/sign-out/") } else { A("Sign in") .href("/sign-in/") } } .class("menu-items") } .id("primary-menu") } .id("navigation") } Main { body } Footer { Section { P { Text("This site is powered by ") A("Swift") .href("https://swift.org") .target(.blank) Text(" & ") A("Vapor") .href("https://vapor.codes") .target(.blank) Text(".") } P("myPage © 2020-2022") } } Script() .type(.javascript) .src("/js/web.js") } } .lang("en-US") } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Web/Templates/Html/WebLinkTemplate.swift ================================================ import Vapor import SwiftHtml struct WebLinkTemplate: TemplateRepresentable { var context: WebLinkContext init( _ context: WebLinkContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { A(context.label) .href(context.url) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Web/WebModule.swift ================================================ import Vapor struct WebModule: ModuleInterface { let router = WebRouter() func boot(_ app: Application) throws { try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Modules/Web/WebRouter.swift ================================================ import Vapor struct WebRouter: RouteCollection { let frontendController = WebFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get(use: frontendController.homeView) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Template/Request+Template.swift ================================================ import Vapor public extension Request { var templates: TemplateRenderer { .init(self) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Template/TemplateRenderer.swift ================================================ import Vapor import SwiftHtml public struct TemplateRenderer { var req: Request init(_ req: Request) { self.req = req } public func renderHtml( _ template: TemplateRepresentable, minify: Bool = false, indent: Int = 4 ) -> Response { let doc = Document(.html) { template.render(req) } let body = DocumentRenderer( minify: minify, indent: indent ) .render(doc) return Response( status: .ok, headers: [ "content-type": "text/html" ], body: .init(string: body) ) } } ================================================ FILE: Chapter 11/myProject/Sources/App/Template/TemplateRepresentable.swift ================================================ import Vapor import SwiftSgml public protocol TemplateRepresentable { @TagBuilder func render(_ req: Request) -> Tag } ================================================ FILE: Chapter 11/myProject/Sources/App/configure.swift ================================================ import Vapor import Fluent import FluentSQLiteDriver import Liquid import LiquidLocalDriver public func configure( _ app: Application ) throws { app.fileStorages.use( .local( publicUrl: "http://localhost:8080", publicPath: app.directory.publicDirectory, workDirectory: "assets" ), as: .local ) app.routes.defaultMaxBodySize = "10mb" let dbPath = app.directory.resourcesDirectory + "db.sqlite" app.databases.use(.sqlite(.file(dbPath)), as: .sqlite) app.middleware.use( FileMiddleware( publicDirectory: app.directory.publicDirectory ) ) app.middleware.use(ExtendPathMiddleware()) app.sessions.use(.fluent) app.migrations.add(SessionRecord.migration) app.middleware.use(app.sessions.middleware) let modules: [ModuleInterface] = [ WebModule(), UserModule(), AdminModule(), BlogModule(), ] for module in modules { try module.boot(app) } try app.autoMigrate().wait() } ================================================ FILE: Chapter 11/myProject/Sources/App/routes.swift ================================================ import Vapor import SwiftHtml func routes(_ app: Application) throws { app.routes.get { req -> Response in req.templates.renderHtml( WebIndexTemplate( WebIndexContext( title: "Home" ) ) { P("Hi there, welcome to my page!") } ) } } ================================================ FILE: Chapter 11/myProject/Sources/Run/main.swift ================================================ import App import Vapor var env = try Environment.detect() try LoggingSystem.bootstrap(from: &env) let app = Application(env) defer { app.shutdown() } try configure(app) try app.run() ================================================ FILE: Chapter 11/myProject/Tests/AppTests/AppTests.swift ================================================ @testable import App import XCTVapor final class AppTests: XCTestCase { func testHelloWorld() throws { let app = Application(.testing) defer { app.shutdown() } try configure(app) try app.test(.GET, "hello", afterResponse: { res in XCTAssertEqual(res.status, .ok) XCTAssertEqual(res.body.string, "Hello, world!") }) } } ================================================ FILE: Chapter 11/myProject/docker-compose.yml ================================================ # 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 # Stop all: docker compose down # x-shared_environment: &shared_environment LOG_LEVEL: ${LOG_LEVEL:-debug} services: app: image: myproject:latest build: context: . environment: <<: *shared_environment 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"] ================================================ FILE: Chapter 12/myProject/Dockerfile ================================================ # ================================ # Build image # ================================ FROM swift:6.0-noble 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 \ $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true) # Copy entire repo into container COPY . . # Build the application, 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 \ --product myProject \ --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)/myProject" ./ # 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:noble # 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 ["./myProject"] CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] ================================================ FILE: Chapter 12/myProject/Package.swift ================================================ // swift-tools-version:5.7 import PackageDescription let package = Package( name: "myProject", platforms: [ .macOS(.v12) ], dependencies: [ .package( url: "https://github.com/vapor/vapor", from: "4.70.0" ), .package( url: "https://github.com/vapor/fluent", from: "4.4.0" ), .package( url: "https://github.com/vapor/fluent-sqlite-driver", from: "4.1.0" ), .package( url: "https://github.com/binarybirds/liquid", from: "1.3.0" ), .package( url: "https://github.com/binarybirds/liquid-local-driver", from: "1.3.0" ), .package( url: "https://github.com/binarybirds/swift-html", from: "1.7.0" ), ], targets: [ .target(name: "App", dependencies: [ .product(name: "Vapor", package: "vapor"), .product(name: "Fluent", package: "fluent"), .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), .product(name: "Liquid", package: "liquid"), .product(name: "LiquidLocalDriver", package: "liquid-local-driver"), .product(name: "SwiftHtml", package: "swift-html"), .product(name: "SwiftSvg", package: "swift-html"), ]), .executableTarget(name: "Run", dependencies: ["App"]), .testTarget(name: "AppTests", dependencies: [ .target(name: "App"), .product(name: "XCTVapor", package: "vapor"), ]) ] ) ================================================ FILE: Chapter 12/myProject/Public/css/admin.css ================================================ tr { column-gap: 1rem; } ================================================ FILE: Chapter 12/myProject/Public/css/web.css ================================================ #blog h2 { margin: 0.5rem 0; } ================================================ FILE: Chapter 12/myProject/Public/js/admin.js ================================================ document.addEventListener("keydown", function(e) { if ( (window.navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey) && e.keyCode == 83 ) { e.preventDefault(); document.forms[0].submit(); } }, false); ================================================ FILE: Chapter 12/myProject/Public/js/web.js ================================================ function about() { alert("myPage\n\nversion 1.0.0"); } ================================================ FILE: Chapter 12/myProject/Sources/App/Extensions/Svg+MenuIcon.swift ================================================ import SwiftSvg extension Svg { static func menuIcon() -> Svg { Svg { Line(x1: 3, y1: 12, x2: 21, y2: 12) Line(x1: 3, y1: 6, x2: 21, y2: 6) Line(x1: 3, y1: 18, x2: 21, y2: 18) } .width(24) .height(24) .viewBox(minX: 0, minY: 0, width: 24, height: 24) .fill("none") .stroke("currentColor") .strokeWidth(2) .strokeLinecap("round") .strokeLinejoin("round") } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/ApiModelInterface.swift ================================================ import Vapor public protocol ApiModelInterface { associatedtype Module: ApiModuleInterface static var pathKey: String { get } static var pathIdKey: String { get } } extension ApiModelInterface { static var pathKey: String { String(describing: self).lowercased() + "s" } static var pathIdKey: String { String(describing: self).lowercased() + "Id" } static var pathIdComponent: PathComponent { .init(stringLiteral: ":" + pathIdKey) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/ApiModuleInterface.swift ================================================ import Foundation public protocol ApiModuleInterface { static var pathKey: String { get } } public extension ApiModuleInterface { static var pathKey: String { String(describing: self).lowercased() } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/AuthenticatedUser.swift ================================================ import Vapor public struct AuthenticatedUser { public let id: UUID public let email: String public init( id: UUID, email: String ) { self.id = id self.email = email } } extension AuthenticatedUser: SessionAuthenticatable { public var sessionID: UUID { id } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Controllers/CreateController.swift ================================================ import Vapor protocol CreateController: ModelController { } extension CreateController { } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Controllers/DeleteController.swift ================================================ import Vapor public protocol DeleteController: ModelController { } public extension DeleteController { } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Controllers/DetailController.swift ================================================ import Vapor protocol DetailController: ModelController { } extension DetailController { } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Controllers/ListController.swift ================================================ import Vapor protocol ListController: ModelController { func list( _ req: Request ) async throws -> [DatabaseModel] } extension ListController { func list( _ req: Request ) async throws -> [DatabaseModel] { try await DatabaseModel .query(on: req.db) .all() } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Controllers/ModelController.swift ================================================ import Vapor import Fluent public struct Name { let singular: String let plural: String init( singular: String, plural: String? = nil ) { self.singular = singular self.plural = plural ?? singular + "s" } } public protocol ModelController { associatedtype ApiModel: ApiModelInterface associatedtype DatabaseModel: DatabaseModelInterface static var moduleName: String { get } static var modelName: Name { get } func identifier( _ req: Request ) throws -> UUID func findBy( _ id: UUID, on: Database ) async throws -> DatabaseModel func getBaseRoutes( _ routes: RoutesBuilder ) -> RoutesBuilder } extension ModelController { static var moduleName: String { DatabaseModel.Module.identifier.capitalized } static var modelName: Name { .init(singular: String(DatabaseModel.identifier.dropLast(1))) } func identifier( _ req: Request ) throws -> UUID { guard let id = req.parameters.get(ApiModel.pathIdKey), let uuid = UUID(uuidString: id) else { throw Abort(.badRequest) } return uuid } func findBy( _ id: UUID, on db: Database ) async throws -> DatabaseModel { guard let model = try await DatabaseModel.find(id, on: db) else { throw Abort(.notFound) } return model } func getBaseRoutes( _ routes: RoutesBuilder ) -> RoutesBuilder { routes .grouped(ApiModel.Module.pathKey.pathComponents) .grouped(ApiModel.pathKey.pathComponents) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Controllers/PatchController.swift ================================================ import Vapor public protocol PatchController: ModelController { } public extension PatchController { } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Controllers/UpdateController.swift ================================================ import Vapor public protocol UpdateController: ModelController { } public extension UpdateController { } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/DatabaseModelInterface.swift ================================================ import Vapor import Fluent public protocol DatabaseModelInterface: Fluent.Model where Self.IDValue == UUID { associatedtype Module: ModuleInterface static var identifier: String { get } } public extension DatabaseModelInterface { static var schema: String { Module.identifier + "_" + identifier } static var identifier: String { String(describing: self) .dropFirst(Module.identifier.count) .dropLast(5) .lowercased() + "s" } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Extensions/ByteBuffer+Data.swift ================================================ import Vapor public extension ByteBuffer { var data: Data? { getData(at: 0, length: readableBytes) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Extensions/File+ByteBuffer.swift ================================================ import Vapor public extension File { var byteBuffer: ByteBuffer { data } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Form/AbstractForm.swift ================================================ import Vapor open class AbstractForm: FormComponent { open var action: FormAction open var fields: [FormComponent] open var error: String? open var submit: String? public init( action: FormAction = .init(), fields: [FormComponent] = [], error: String? = nil, submit: String? = nil ) { self.action = action self.fields = fields self.error = error self.submit = submit self.action.enctype = .multipart } open func load(req: Request) async throws { for field in fields { try await field.load(req: req) } } open func process(req: Request) async throws { for field in fields { try await field.process(req: req) } } open func validate(req: Request) async throws -> Bool { var result: [Bool] = [] for field in fields { result.append(try await field.validate(req: req)) } return result.filter { $0 == false }.isEmpty } open func write(req: Request) async throws { for field in fields { try await field.write(req: req) } } open func save(req: Request) async throws { for field in fields { try await field.save(req: req) } } open func read(req: Request) async throws { for field in fields { try await field.read(req: req) } } open func render(req: Request) -> TemplateRepresentable { FormTemplate(getContext(req)) } func getContext(_ req: Request) -> FormContext { .init( action: action, fields: fields.map { $0.render(req: req)}, error: error, submit: submit ) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Form/AbstractFormField.swift ================================================ import Vapor open class AbstractFormField< Input: Decodable, Output: TemplateRepresentable >: FormComponent { public var key: String public var input: Input public var output: Output public var error: String? // MARK: - event blocks public typealias FormFieldBlock = (Request, AbstractFormField) async throws -> Void public typealias FormFieldValidatorsBlock = ((Request, AbstractFormField) -> [AsyncValidator]) private var readBlock: FormFieldBlock? private var writeBlock: FormFieldBlock? private var loadBlock: FormFieldBlock? private var saveBlock: FormFieldBlock? private var validatorsBlock: FormFieldValidatorsBlock? // MARK: - init & config public init( key: String, input: Input, output: Output, error: String? = nil ) { self.key = key self.input = input self.output = output self.error = error } open func config( _ block: (AbstractFormField) -> Void ) -> Self { block(self) return self } // MARK: - Block setters open func read(_ block: @escaping FormFieldBlock) -> Self { readBlock = block return self } open func write(_ block: @escaping FormFieldBlock) -> Self { writeBlock = block return self } open func load(_ block: @escaping FormFieldBlock) -> Self { loadBlock = block return self } open func save(_ block: @escaping FormFieldBlock) -> Self { saveBlock = block return self } open func validators( @AsyncValidatorBuilder _ block: @escaping FormFieldValidatorsBlock ) -> Self { validatorsBlock = block return self } // MARK: - FormComponent open func process(req: Request) async throws { if let value = try? req.content.get(Input.self, at: key) { input = value } } open func validate(req: Request) async throws -> Bool { guard let validators = validatorsBlock else { return true } return await RequestValidator(validators(req, self)).isValid(req) } open func read(req: Request) async throws { try await readBlock?(req, self) } open func write(req: Request) async throws { try await writeBlock?(req, self) } open func load(req: Request) async throws { try await loadBlock?(req, self) } open func save(req: Request) async throws { try await saveBlock?(req, self) } open func render(req: Request) -> TemplateRepresentable { output } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Form/Fields/HiddenField.swift ================================================ import Vapor public final class HiddenField: AbstractFormField< String, HiddenFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Form/Fields/ImageField.swift ================================================ import Vapor public final class ImageField: AbstractFormField< FormImageInput, ImageFieldTemplate > { public var imageKey: String? { didSet { output.context.data.originalKey = imageKey } } public var path: String public init( _ key: String, path: String ) { self.path = path super.init( key: key, input: .init(key: key), output: .init(.init(key: key)) ) } public override func process( req: Request ) async throws { /// process input input.file = try? req.content.get( File.self, at: key ) input.data.originalKey = try? req.content.get( String.self, at: key + "OriginalKey" ) if let temporaryFileKey = try? req.content.get( String.self, at: key + "TemporaryFileKey" ), let temporaryFileName = try? req.content.get( String.self, at: key + "TemporaryFileName" ) { input.data.temporaryFile = .init( key: temporaryFileKey, name: temporaryFileName ) } input.data.shouldRemove = (try? req.content.get( Bool.self, at: key + "ShouldRemove" )) ?? false /// remove & upload file if input.data.shouldRemove { if let originalKey = input.data.originalKey { try? await req.fs.delete(key: originalKey) } } else if let file = input.file, let data = file.byteBuffer.data, !data.isEmpty { if let tmpKey = input.data.temporaryFile?.key { try? await req.fs.delete(key: tmpKey) } let key = "tmp/\(UUID().uuidString).tmp" _ = try await req.fs.upload(key: key, data: data) /// update the temporary image input.data.temporaryFile = .init( key: key, name: file.filename ) } /// update output values output.context.data = input.data } public override func write( req: Request ) async throws { imageKey = input.data.originalKey if input.data.shouldRemove { if let key = input.data.originalKey { try? await req.fs.delete(key: key) } imageKey = nil } else if let file = input.data.temporaryFile { var newKey = path + "/" + file.name if await req.fs.exists(key: newKey) { let formatter = DateFormatter() formatter.dateFormat="y-MM-dd-HH-mm-ss-" let prefix = formatter.string(from: .init()) newKey = path + "/" + prefix + file.name } _ = try await req.fs.move(key: file.key, to: newKey) input.data.temporaryFile = nil if let key = input.data.originalKey { try? await req.fs.delete(key: key) } imageKey = newKey } try await super.write(req: req) } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Form/Fields/InputField.swift ================================================ public final class InputField: AbstractFormField< String, InputFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init( key: key ) ) ) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Form/Fields/SelectField.swift ================================================ import Vapor public final class SelectField: AbstractFormField< String, SelectFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Form/Fields/TextareaField.swift ================================================ import Vapor public final class TextareaField: AbstractFormField< String, TextareaFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Form/FormAction.swift ================================================ import SwiftHtml public struct FormAction { public var method: SwiftHtml.Method public var url: String? public var enctype: SwiftHtml.Enctype? public init( method: SwiftHtml.Method = .post, url: String? = nil, enctype: SwiftHtml.Enctype? = nil ) { self.method = method self.url = url self.enctype = enctype } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Form/FormComponent.swift ================================================ import Vapor public protocol FormComponent { func load(req: Request) async throws func process(req: Request) async throws func validate(req: Request) async throws -> Bool func write(req: Request) async throws func save(req: Request) async throws func read(req: Request) async throws func render(req: Request) -> TemplateRepresentable } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Form/FormComponentBuilder.swift ================================================ @resultBuilder public enum FormComponentBuilder { public static func buildBlock( _ components: FormComponent... ) -> [FormComponent] { components } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Form/FormImageData.swift ================================================ public struct FormImageData: Codable { public struct TemporaryFile: Codable { public let key: String public let name: String public init( key: String, name: String ) { self.key = key self.name = name } } public var originalKey: String? public var temporaryFile: TemporaryFile? public var shouldRemove: Bool public init( originalKey: String? = nil, temporaryFile: TemporaryFile? = nil, shouldRemove: Bool = false ) { self.originalKey = originalKey self.temporaryFile = temporaryFile self.shouldRemove = shouldRemove } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Form/FormImageInput.swift ================================================ import Vapor public struct FormImageInput: Codable { public var key: String public var file: File? public var data: FormImageData public init( key: String, file: File? = nil, data: FormImageData? = nil ) { self.key = key self.file = file self.data = data ?? .init() } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/ModelEditorInterface.swift ================================================ import Vapor public protocol ModelEditorInterface: FormComponent { associatedtype Model: DatabaseModelInterface var model: Model { get } var form: AbstractForm { get } init(model: Model, form: AbstractForm) @FormComponentBuilder var formFields: [FormComponent] { get } } public extension ModelEditorInterface { func load(req: Request) async throws { try await form.load(req: req) } func process(req: Request) async throws { try await form.process(req: req) } func validate(req: Request) async throws -> Bool { try await form.validate(req: req) } func write(req: Request) async throws { try await form.write(req: req) } func save(req: Request) async throws { try await form.save(req: req) } func read(req: Request) async throws { try await form.read(req: req) } func render(req: Request) -> TemplateRepresentable { form.render(req: req) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/ModuleInterface.swift ================================================ import Vapor public protocol ModuleInterface { static var identifier: String { get } func boot(_ app: Application) throws } public extension ModuleInterface { func boot(_ app: Application) throws {} static var identifier: String { String(describing: self).dropLast(6).lowercased() } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Contexts/CellContext.swift ================================================ public struct CellContext { public enum `Type`: String { case text case image } public let value: String public let link: LinkContext? public let type: `Type` public init( _ value: String, link: LinkContext? = nil, type: `Type` = .text ) { self.type = type self.value = value self.link = link } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Contexts/ColumnContext.swift ================================================ public struct ColumnContext { public let key: String public let label: String public init( _ key: String, label: String? = nil ) { self.key = key self.label = label ?? key.capitalized } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Contexts/DetailContext.swift ================================================ public struct DetailContext { public enum `Type`: String { case text case image } public let key: String public let label: String public let value: String public let type: `Type` public init( _ key: String, _ value: String, label: String? = nil, type: `Type` = .text ) { self.key = key self.label = label ?? key.capitalized self.value = value self.type = type } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Contexts/FormContext.swift ================================================ public struct FormContext { public var action: FormAction public var fields: [TemplateRepresentable] public var error: String? public var submit: String? } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Contexts/HiddenFieldContext.swift ================================================ public struct HiddenFieldContext { public let key: String public var value: String? public init( key: String, value: String? = nil ) { self.key = key self.value = value } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Contexts/ImageFieldContext.swift ================================================ public struct ImageFieldContext { public let key: String public var label: LabelContext public var data: FormImageData public var previewUrl: String? public var accept: String? public var error: String? public init( key: String, label: LabelContext? = nil, data: FormImageData = .init(), previewUrl: String? = nil, accept: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.data = data self.previewUrl = previewUrl self.accept = accept self.error = error } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Contexts/InputFieldContext.swift ================================================ import SwiftHtml public struct InputFieldContext { public let key: String public var label: LabelContext public var type: SwiftHtml.Input.`Type` public var placeholder: String? public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, type: Input.`Type` = .text, placeholder: String? = nil, value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.type = type self.placeholder = placeholder self.value = value self.error = error } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Contexts/LabelContext.swift ================================================ public struct LabelContext { public var key: String public var title: String? public var required: Bool public var more: String? public init( key: String, title: String? = nil, required: Bool = false, more: String? = nil ) { self.key = key self.title = title self.required = required self.more = more } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Contexts/LinkContext.swift ================================================ import Vapor public struct LinkContext { public let label: String public let path: String public let absolute: Bool public let isBlank: Bool public let dropLast: Int public init( label: String, path: String = "", absolute: Bool = false, isBlank: Bool = false, dropLast: Int = 0 ) { self.label = label self.path = path self.absolute = absolute self.isBlank = isBlank self.dropLast = dropLast } public func url( _ req: Request, _ infix: [PathComponent] = [] ) -> String { if absolute { return path } return "/" + (req.url.path.pathComponents.dropLast(dropLast) + (infix + path.pathComponents)).string } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Contexts/OptionContext.swift ================================================ public struct OptionContext { public var key: String public var label: String public init( key: String, label: String ) { self.key = key self.label = label } } public extension OptionContext { static func yesNo() -> [OptionContext] { ["yes", "no"].map { .init(key: $0, label: $0.capitalized) } } static func trueFalse() -> [OptionContext] { [true, false].map { .init(key: String($0), label: String($0).capitalized) } } static func numbers( _ numbers: [Int] ) -> [OptionContext] { numbers.map { .init(key: String($0), label: String($0)) } } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Contexts/RowContext.swift ================================================ public struct RowContext { public let id: String public let cells: [CellContext] public init( id: String, cells: [CellContext] ) { self.id = id self.cells = cells } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Contexts/SelectFieldContext.swift ================================================ public struct SelectFieldContext { public let key: String public var label: LabelContext public var options: [OptionContext] public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, options: [OptionContext] = [], value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.options = options self.value = value self.error = error } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Contexts/TableContext.swift ================================================ public struct TableContext { public let columns: [ColumnContext] public let rows: [RowContext] public let actions: [LinkContext] public init( columns: [ColumnContext], rows: [RowContext], actions: [LinkContext] = [] ) { self.columns = columns self.rows = rows self.actions = actions } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Contexts/TextareaFieldContext.swift ================================================ public struct TextareaFieldContext { public let key: String public var label: LabelContext public var placeholder: String? public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, placeholder: String? = nil, value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.placeholder = placeholder self.value = value self.error = error } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Html/CellTemplate.swift ================================================ import Vapor import SwiftHtml public struct CellTemplate: TemplateRepresentable { var context: CellContext var rowId: String public init( _ context: CellContext, rowId: String ) { self.context = context self.rowId = rowId } @TagBuilder public func render( _ req: Request ) -> Tag { Td { switch context.type { case .text: if let link = context.link { LinkTemplate(link, pathInfix: rowId).render(req) } else { Text(context.value) } case .image: if let link = context.link { LinkTemplate(link, pathInfix: rowId) { label in Img(src: context.value, alt: label) } .render(req) } else { Img(src: context.value, alt: context.value) } } } .class("field") } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Html/DetailTemplate.swift ================================================ import Vapor import SwiftHtml public struct DetailTemplate: TemplateRepresentable { var context: DetailContext public init( _ context: DetailContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Dt(context.label) switch context.type { case .text: if context.value.isEmpty { Dd(" ") } else { Dd( context.value.replacingOccurrences( of: "\n", with: "
" ) ) } case .image: Dd { Img( src: context.value, alt: context.label ) } } } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Html/FormTemplate.swift ================================================ import Vapor import SwiftHtml public struct FormTemplate: TemplateRepresentable { var context: FormContext public init( _ context: FormContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Form { if let error = context.error { Section { P(error) .class("error") } } for field in context.fields { Section { field.render(req) } } Section { Input() .type(.submit) .value(context.submit ?? "Save") } } .method(context.action.method) .action(context.action.url) .enctype(context.action.enctype) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Html/HiddenFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct HiddenFieldTemplate: TemplateRepresentable { var context: HiddenFieldContext public init( _ context: HiddenFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Input() .type(.hidden) .name(context.key) .value(context.value) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Html/ImageFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct ImageFieldTemplate: TemplateRepresentable { public var context: ImageFieldContext public init( _ context: ImageFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { if let url = context.previewUrl { Img(src: req.fs.resolve(key: url), alt: context.key) } else { Img(src: "/img/logo.png", alt: "default post image") } LabelTemplate(context.label).render(req) Input() .type(.file) .key(context.key) .class("field") .accept(context.accept) if let temporaryFile = context.data.temporaryFile { Input() .key(context.key + "TemporaryFileKey") .value(temporaryFile.key) .type(.hidden) Input() .key(context.key + "TemporaryFileName") .value(temporaryFile.name) .type(.hidden) } if let key = context.data.originalKey { Input() .key(context.key + "OriginalKey") .value(key) .type(.hidden) } if !context.label.required { Input() .key(context.key + "ShouldRemove") .value(String(true)) .type(.checkbox) .checked(context.data.shouldRemove) Label("Remove") .for(context.key + "Remove") } if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Html/InputFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct InputFieldTemplate: TemplateRepresentable { public var context: InputFieldContext public init( _ context: InputFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Input() .type(context.type) .key(context.key) .placeholder(context.placeholder) .value(context.value) .class("field") if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Html/LabelTemplate.swift ================================================ import Vapor import SwiftHtml public struct LabelTemplate: TemplateRepresentable { var context: LabelContext public init( _ context: LabelContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Label { Text(context.title ?? context.key.capitalized) if let more = context.more { Span(more) .class("more") } if context.required { Span("*") .class("required") } }.for(context.key) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Html/LinkTemplate.swift ================================================ import Vapor import SwiftHtml public struct LinkTemplate: TemplateRepresentable { var context: LinkContext var body: Tag var pathInfix: String? public init( _ context: LinkContext, pathInfix: String? = nil, _ builder: ((String) -> Tag)? = nil ) { self.context = context self.pathInfix = pathInfix self.body = builder?(context.label) ?? Text(context.label) } @TagBuilder public func render( _ req: Request ) -> Tag { A { body } .href(context.url(req, pathInfix?.pathComponents ?? [])) .target(.blank, context.isBlank) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Html/SelectFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct SelectFieldTemplate: TemplateRepresentable { public var context: SelectFieldContext public init( _ context: SelectFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Select { for item in context.options { Option(item.label) .value(item.key) .selected(context.value == item.key) } } .name(context.key) if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Html/TableTemplate.swift ================================================ import Vapor import SwiftHtml public struct TableTemplate: TemplateRepresentable { var context: TableContext public init( _ context: TableContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Table { Thead { Tr { context.columns.map { column in Th(column.label) .id(column.key) .class("field") } context.actions.map { action in Th(action.label) .class("action") } } } Tbody { for row in context.rows { Tr { row.cells.map { CellTemplate($0, rowId: row.id).render(req) } context.actions.map { action in Td { LinkTemplate(action, pathInfix: row.id).render(req) } .class("action") } } .id(row.id) } } } } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Templates/Html/TextareaFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct TextareaFieldTemplate: TemplateRepresentable { public var context: TextareaFieldContext public init( _ context: TextareaFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Textarea(context.value) .placeholder(context.placeholder) .name(context.key) if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Validation/AsyncValidator.swift ================================================ import Vapor public protocol AsyncValidator { var key: String { get } var message: String { get } func validate( _ req: Request ) async throws -> ValidationErrorDetail? } public extension AsyncValidator { var error: ValidationErrorDetail { .init(key: key, message: message) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Validation/AsyncValidatorBuilder.swift ================================================ @resultBuilder public enum AsyncValidatorBuilder { public static func buildBlock( _ components: AsyncValidator... ) -> [AsyncValidator] { components } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Validation/FormFieldValidator+Validations.swift ================================================ import Vapor public extension FormFieldValidator where Input == String { static func required( _ field: AbstractFormField, _ message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is required" return .init(field, msg) { _, field in !field.input.isEmpty } } static func min( _ field: AbstractFormField, length: Int, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)" return .init(field, msg) { _, field in field.input.count >= length } } static func max( _ field: AbstractFormField, length: Int, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)" return .init(field, msg) { _, field in field.input.count <= length } } static func alphanumeric( _ field: AbstractFormField, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) should be only alphanumeric characters" return .init(field, msg) { _, field in !Validator .characterSet(.alphanumerics) .validate(field.input) .isFailure } } static func email( _ field: AbstractFormField, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) should be a valid email address" return .init(field, msg) { _, field in !Validator .email .validate(field.input) .isFailure } } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Validation/FormFieldValidator.swift ================================================ import Vapor public struct FormFieldValidator< Input: Decodable, Output: TemplateRepresentable >: AsyncValidator { public typealias AsyncValidationBlock = ((Request, AbstractFormField) async throws -> Bool) public let field: AbstractFormField public let message: String public let validation: AsyncValidationBlock public var key: String { field.key } public init( _ field: AbstractFormField, _ message: String, _ validation: @escaping AsyncValidationBlock ) { self.field = field self.message = message self.validation = validation } public func validate( _ req: Request ) async throws -> ValidationErrorDetail? { let isValid = try await validation(req, field) if isValid { return nil } field.error = message return error } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Validation/RequestValidator.swift ================================================ import Vapor public struct RequestValidator { public var validators: [AsyncValidator] public init( _ validators: [AsyncValidator] ) { self.validators = validators } public func validate( _ req: Request, message: String? = nil ) async throws { var result: [ValidationErrorDetail] = [] for validator in validators { if result.contains(where: { $0.key == validator.key }) { continue } if let res = try await validator.validate(req) { result.append(res) } } if !result.isEmpty { throw ValidationAbort( abort: Abort(.badRequest, reason: message), details: result ) } } public func isValid( _ req: Request ) async -> Bool { do { try await validate(req, message: nil) return true } catch { return false } } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Validation/ValidationAbort.swift ================================================ import Vapor public struct ValidationAbort: AbortError { public var abort: Abort public var message: String? public var details: [ValidationErrorDetail] public var reason: String { abort.reason } public var status: HTTPStatus { abort.status } public init( abort: Abort, message: String? = nil, details: [ValidationErrorDetail] ) { self.abort = abort self.message = message self.details = details } } ================================================ FILE: Chapter 12/myProject/Sources/App/Framework/Validation/ValidationErrorDetail.swift ================================================ import Vapor public struct ValidationErrorDetail: Codable { public var key: String public var message: String public init( key: String, message: String ) { self.key = key self.message = message } } extension ValidationErrorDetail: Content {} ================================================ FILE: Chapter 12/myProject/Sources/App/Middlewares/ExtendPathMiddleware.swift ================================================ import Vapor struct ExtendPathMiddleware: AsyncMiddleware { func respond( to req: Request, chainingTo next: AsyncResponder ) async throws -> Response { if !req.url.path.hasSuffix("/") && !req.url.path.contains(".") { return req.redirect( to: req.url.path + "/", redirectType: .permanent ) } return try await next.respond( to: req ) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Admin/AdminModule.swift ================================================ import Vapor struct AdminModule: ModuleInterface { let router = AdminRouter() func boot(_ app: Application) throws { try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Admin/AdminRouter.swift ================================================ import Vapor struct AdminRouter: RouteCollection { let controller = AdminFrontendController() func boot(routes: RoutesBuilder) throws { routes .grouped( AuthenticatedUser.redirectMiddleware( path: "/sign-in/" ) ) .get("admin", use: controller.dashboardView) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Admin/Controllers/AdminController.swift ================================================ import Vapor protocol AdminController: AdminListController, AdminDetailController, AdminCreateController, AdminUpdateController, AdminDeleteController { func setupRoutes( _ routes: RoutesBuilder ) } extension AdminController { func setupRoutes( _ routes: RoutesBuilder ) { setupListRoutes(routes) setupDetailRoutes(routes) setupCreateRoutes(routes) setupUpdateRoutes(routes) setupDeleteRoutes(routes) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Admin/Controllers/AdminCreateController.swift ================================================ import Vapor protocol AdminCreateController: CreateController { associatedtype CreateModelEditor: ModelEditorInterface func createTemplate( _ req: Request, _ editor: CreateModelEditor ) -> TemplateRepresentable func createView( _ req: Request ) async throws -> Response func createAction( _ req: Request ) async throws -> Response func createContext( _ req: Request, _ editor: CreateModelEditor ) -> AdminEditorPageContext func createBreadcrumbs( _ req: Request ) -> [LinkContext] func setupCreateRoutes( _ routes: RoutesBuilder ) } extension AdminCreateController { private func render( _ req: Request, editor: CreateModelEditor ) -> Response { req.templates.renderHtml( createTemplate(req, editor) ) } func createView( _ req: Request ) async throws -> Response { let editor = CreateModelEditor( model: .init(), form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) return render(req, editor: editor) } func createAction( _ req: Request ) async throws -> Response { let model = DatabaseModel() let editor = CreateModelEditor( model: model as! CreateModelEditor.Model, form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) try await editor.process(req: req) let isValid = try await editor.validate(req: req) guard isValid else { return render(req, editor: editor) } try await editor.write(req: req) try await editor.model.create(on: req.db) try await editor.save(req: req) var components = req.url.path.pathComponents.dropLast() components += editor.model.id!.uuidString.pathComponents return req.redirect(to: "/" + components.string + "/update/") } func createTemplate( _ req: Request, _ editor: CreateModelEditor ) -> TemplateRepresentable { AdminEditorPageTemplate( createContext(req, editor) ) } func createContext( _ req: Request, _ editor: CreateModelEditor ) -> AdminEditorPageContext { let context = FormContext( action: editor.form.action, fields: editor.form.fields.map { $0.render(req: req) }, error: editor.form.error, submit: editor.form.submit ) return .init( title: "Create", form: context, breadcrumbs: createBreadcrumbs(req) ) } func createBreadcrumbs( _ req: Request ) -> [LinkContext] { [ LinkContext( label: Self.moduleName.capitalized, dropLast: 2 ), LinkContext( label: Self.modelName.plural.capitalized, dropLast: 1 ), ] } func setupCreateRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) baseRoutes.get("create", use: createView) baseRoutes.post("create", use: createAction) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Admin/Controllers/AdminDeleteController.swift ================================================ import Vapor final class DeleteForm: AbstractForm { init() { super.init() self.action.method = .post self.submit = "Delete" } } protocol AdminDeleteController: DeleteController { func deleteView( _ req: Request ) async throws -> Response func deleteAction( _ req: Request ) async throws -> Response func deleteTemplate( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> TemplateRepresentable func deleteInfo( _ model: DatabaseModel ) -> String func deleteContext( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> AdminDeletePageContext func setupDeleteRoutes( _ routes: RoutesBuilder ) } extension AdminDeleteController { func deleteView( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let form = DeleteForm() return req.templates.renderHtml( deleteTemplate(req, model, form) ) } func deleteAction( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) try await model.delete(on: req.db) var url = req.url.path if let redirect = try? req.query.get(String.self, at: "redirect") { url = redirect } return req.redirect(to: url) } func deleteTemplate( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> TemplateRepresentable { AdminDeletePageTemplate( deleteContext(req, model, form) ) } func deleteContext( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> AdminDeletePageContext { .init( title: "Delete", name: deleteInfo(model), type: "model", form: form.getContext(req) ) } func setupDeleteRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.get("delete", use: deleteView) existingModelRoutes.post("delete", use: deleteAction) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Admin/Controllers/AdminDetailController.swift ================================================ import Vapor protocol AdminDetailController: DetailController { func detailView( _ req: Request ) async throws -> Response func detailTemplate( _ req: Request, _ model: DatabaseModel ) -> TemplateRepresentable func detailFields( for model: DatabaseModel ) -> [DetailContext] func detailContext( _ req: Request, _ model: DatabaseModel ) -> AdminDetailPageContext func detailBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func detailNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func setupDetailRoutes( _ routes: RoutesBuilder ) } extension AdminDetailController { func detailView( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) return req.templates.renderHtml(detailTemplate(req, model)) } func detailTemplate( _ req: Request, _ model: DatabaseModel ) -> TemplateRepresentable { AdminDetailPageTemplate(detailContext(req, model)) } func detailContext( _ req: Request, _ model: DatabaseModel ) -> AdminDetailPageContext { let path = "/delete/?redirect=" + req.url.path.pathComponents.dropLast().string + "&cancel=" + req.url.path return .init( title: "Details", fields: detailFields(for: model), navigation: detailNavigation(req, model), breadcrumbs: detailBreadcrumbs(req, model), actions: [ LinkContext( label: "Delete", path: path ), ] ) } func detailBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: Self.moduleName.capitalized, dropLast: 2 ), LinkContext( label: Self.modelName.plural.capitalized, dropLast: 1 ), ] } func detailNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: "Update", path: "update" ), ] } func setupDetailRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.get(use: detailView) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Admin/Controllers/AdminFrontendController.swift ================================================ import Vapor struct AdminFrontendController { func dashboardView( req: Request ) throws -> Response { let user = try req.auth.require(AuthenticatedUser.self) let template = AdminDashboardTemplate( .init( icon: "👋", title: "Dashboard", message: "Hello \(user.email), welcome to the CMS." ) ) return req.templates.renderHtml(template) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Admin/Controllers/AdminListController.swift ================================================ import Vapor protocol AdminListController: ListController { func listView( _ req: Request ) async throws -> Response func listColumns() -> [ColumnContext] func listCells( for model: DatabaseModel ) -> [CellContext] func listNavigation( _ req: Request ) -> [LinkContext] func listBreadcrumbs( _ req: Request ) -> [LinkContext] func listContext( _ req: Request, _ list: [DatabaseModel] ) -> AdminListPageContext func listTemplate( _ req: Request, _ list: [DatabaseModel] ) -> TemplateRepresentable func setupListRoutes( _ routes: RoutesBuilder ) } extension AdminListController { func listView(_ req: Request) async throws -> Response { let list = try await list(req) let template = listTemplate(req, list) return req.templates.renderHtml(template) } func listNavigation(_ req: Request) -> [LinkContext] { [ LinkContext(label: "Create",path: "create") ] } func listBreadcrumbs(_ req: Request) -> [LinkContext] { [ LinkContext( label: DatabaseModel.Module.identifier.capitalized, dropLast: 1 ) ] } func listContext( _ req: Request, _ list: [DatabaseModel] ) -> AdminListPageContext { let rows = list.map { RowContext(id: $0.id!.uuidString, cells: listCells(for: $0)) } let table = TableContext(columns: listColumns(), rows: rows, actions: [ LinkContext(label: "Update", path: "update"), LinkContext(label: "Delete", path: "delete") ]) return .init( title: "List", table: table, navigation: listNavigation(req), breadcrumbs: listBreadcrumbs(req) ) } func listTemplate( _ req: Request, _ list: [DatabaseModel] ) -> TemplateRepresentable { AdminListPageTemplate(listContext(req, list)) } func setupListRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) baseRoutes.get(use: listView) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Admin/Controllers/AdminUpdateController.swift ================================================ import Vapor protocol AdminUpdateController: UpdateController { associatedtype UpdateModelEditor: ModelEditorInterface func updateView( _ req: Request ) async throws -> Response func updateAction( _ req: Request ) async throws -> Response func updateTemplate( _ req: Request, _ editor: UpdateModelEditor ) async -> TemplateRepresentable func updateContext( _ req: Request, _ editor: UpdateModelEditor ) async -> AdminEditorPageContext func updateBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func updateNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func setupUpdateRoutes( _ routes: RoutesBuilder ) } extension AdminUpdateController { private func render( _ req: Request, editor: UpdateModelEditor ) async -> Response { req.templates.renderHtml( await updateTemplate(req, editor) ) } func updateView( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let editor = UpdateModelEditor( model: model as! UpdateModelEditor.Model, form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) try await editor.read(req: req) return await render(req, editor: editor) } func updateAction( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let editor = UpdateModelEditor( model: model as! UpdateModelEditor.Model, form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) try await editor.process(req: req) let isValid = try await editor.validate(req: req) guard isValid else { return await render(req, editor: editor) } try await editor.write(req: req) try await editor.model.update(on: req.db) try await editor.save(req: req) return req.redirect(to: req.url.path) } func updateTemplate( _ req: Request, _ editor: UpdateModelEditor ) async -> TemplateRepresentable { await AdminEditorPageTemplate( updateContext(req, editor) ) } func updateContext( _ req: Request, _ editor: UpdateModelEditor ) async -> AdminEditorPageContext { let path = "delete/?redirect=" + req.url.path.pathComponents.dropLast(2).string + "&cancel=" + req.url.path let model = editor.model as! DatabaseModel return .init( title: "Update", form: editor.form.getContext(req), navigation: updateNavigation(req, model), breadcrumbs: updateBreadcrumbs(req, model), actions: [ LinkContext( label: "Delete", path: path, dropLast: 1 ), ] ) } func updateBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: Self.moduleName.capitalized, dropLast: 3 ), LinkContext( label: Self.modelName.plural.capitalized, dropLast: 2 ), ] } func updateNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: "Details", dropLast: 1 ), ] } func setupUpdateRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.get("update", use: updateView) existingModelRoutes.post("update", use: updateAction) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDashboardContext.swift ================================================ struct AdminDashboardContext { let icon: String let title: String let message: String } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDeletePageContext.swift ================================================ public struct AdminDeletePageContext { public let title: String public let name: String public let type: String public let form: FormContext public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public init( title: String, name: String, type: String, form: FormContext, navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [] ) { self.title = title self.name = name self.type = type self.form = form self.navigation = navigation self.breadcrumbs = breadcrumbs } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDetailPageContext.swift ================================================ public struct AdminDetailPageContext { public let title: String public let fields: [DetailContext] public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public let actions: [LinkContext] public init( title: String, fields: [DetailContext], navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [], actions: [LinkContext] = [] ) { self.title = title self.fields = fields self.navigation = navigation self.breadcrumbs = breadcrumbs self.actions = actions } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminEditorPageContext.swift ================================================ public struct AdminEditorPageContext { public let title: String public let form: FormContext public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public let actions: [LinkContext] public init( title: String, form: FormContext, navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [], actions: [LinkContext] = [] ) { self.title = title self.form = form self.navigation = navigation self.breadcrumbs = breadcrumbs self.actions = actions } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminIndexContext.swift ================================================ public struct AdminIndexContext { public let title: String public let breadcrumbs: [LinkContext] public init( title: String, breadcrumbs: [LinkContext] = [] ) { self.title = title self.breadcrumbs = breadcrumbs } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminListPageContext.swift ================================================ public struct AdminListPageContext { public let title: String public let table: TableContext public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public init( title: String, table: TableContext, navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [] ) { self.title = title self.table = table self.navigation = navigation self.breadcrumbs = breadcrumbs } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDashboardTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminDashboardTemplate: TemplateRepresentable { var context: AdminDashboardContext init( _ context: AdminDashboardContext ) { self.context = context } @TagBuilder func render(_ req: Request) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } Nav { H2("Blog") Ul { Li { A("Posts") .href("/admin/blog/posts/") } Li { A("Categories") .href("/admin/blog/categories/") } } } } .id("dashboard") .class("container") } .render(req) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDeletePageTemplate.swift ================================================ import Vapor import SwiftHtml public struct AdminDeletePageTemplate: TemplateRepresentable { var context: AdminDeletePageContext public init( _ context: AdminDeletePageContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { Span("🗑") .class("icon") H1(context.title) P("You are about to permanently delete the
`\(context.name)` \(context.type).") FormTemplate(context.form).render(req) A("Cancel") .href((try? req.query.get(String.self, at: "cancel")) ?? "#") .class(["button", "cancel"]) } .class(["lead", "container", "center"]) } .render(req) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDetailPageTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminDetailPageTemplate: TemplateRepresentable { var context: AdminDetailPageContext init( _ context: AdminDetailPageContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { Div { H1(context.title) for item in context.navigation { LinkTemplate(item).render(req) } } .class("lead") Dl { for item in context.fields { DetailTemplate(item).render(req) } } Section { for item in context.actions { LinkTemplate(item).render(req) } } } .class("container") } .render(req) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Admin/Templates/Html/AdminEditorPageTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminEditorPageTemplate: TemplateRepresentable { var context: AdminEditorPageContext init( _ context: AdminEditorPageContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { Div { H1(context.title) for item in context.navigation { LinkTemplate(item).render(req) } } .class("lead") FormTemplate(context.form).render(req) Section { for item in context.actions { LinkTemplate(item).render(req) } } } .class("container") } .render(req) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Admin/Templates/Html/AdminIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg public struct AdminIndexTemplate: TemplateRepresentable { public var context: AdminIndexContext var body: Tag public init( _ context: AdminIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Meta() .name("robots") .content("noindex") Link(rel: .shortcutIcon) .href("/images/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css") Link(rel: .stylesheet) .href("/css/admin.css") Title(context.title) } Body { Div { A { Img(src: "/img/logo.png", alt: "Logo") .title("Logo") .style("width: 300px") } .href("/") Nav { Input() .type(.checkbox) .id("secondary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("secondary-menu-button") Div { A("Sign out") .href("/sign-out/") } .class("menu-items") } .id("secondary-menu") } .id("navigation") Div { Nav { A("Admin") .href("/admin/") for breadcrumb in context.breadcrumbs { LinkTemplate(breadcrumb).render(req) } } } .class("breadcrumb") Main { body } Script() .type(.javascript) .src("/js/admin.js") } } .lang("en-US") } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Admin/Templates/Html/AdminListPageTemplate.swift ================================================ import Vapor import SwiftHtml public struct AdminListPageTemplate: TemplateRepresentable { var context: AdminListPageContext public init( _ context: AdminListPageContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { H1(context.title) P { context.navigation.map { LinkTemplate($0).render(req) } } } .class("lead") if context.table.rows.isEmpty { Div { Span("🔍") .class("icon") H2("Oh no") P("This list is empty right now.") A("Try again →") .href(req.url.path) .class("button-1") } .class(["lead", "container", "center"]) } else { TableTemplate(context.table).render(req) } } .render(req) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Api/Controllers/ApiController.swift ================================================ import Vapor protocol ApiController: ApiListController, ApiDetailController, ApiCreateController, ApiUpdateController, ApiPatchController, ApiDeleteController { func setupRoutes( _ routes: RoutesBuilder ) } extension ApiController { func createResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response { try await detailOutput(req, model) .encodeResponse(status: .created, for: req) } func updateResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response { try await detailOutput(req, model) .encodeResponse(for: req) } func patchResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response { try await detailOutput(req, model) .encodeResponse(for: req) } func setupRoutes( _ routes: RoutesBuilder ) { setupListRoutes(routes) setupDetailRoutes(routes) setupCreateRoutes(routes) setupUpdateRoutes(routes) setupPatchRoutes(routes) setupDeleteRoutes(routes) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Api/Controllers/ApiCreateController.swift ================================================ import Vapor protocol ApiCreateController: CreateController { associatedtype CreateObject: Decodable func createInput( _ req: Request, _ model: DatabaseModel, _ input: CreateObject ) async throws func createApi( _ req: Request ) async throws -> Response func createResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response func setupCreateRoutes( _ routes: RoutesBuilder ) } extension ApiCreateController { func createApi( _ req: Request ) async throws -> Response { let input = try req.content.decode(CreateObject.self) let model = DatabaseModel() try await createInput(req, model, input) try await model.create(on: req.db) return try await createResponse(req, model) } func setupCreateRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) baseRoutes.post(use: createApi) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Api/Controllers/ApiDeleteController.swift ================================================ import Vapor public protocol ApiDeleteController: DeleteController { func deleteApi( _ req: Request ) async throws -> HTTPStatus func setupDeleteRoutes( _ routes: RoutesBuilder ) } public extension ApiDeleteController { func deleteApi( _ req: Request ) async throws -> HTTPStatus { let model = try await findBy(identifier(req), on: req.db) try await model.delete(on: req.db) return .noContent } func setupDeleteRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.delete(use: deleteApi) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Api/Controllers/ApiDetailController.swift ================================================ import Vapor protocol ApiDetailController: DetailController { associatedtype DetailObject: Content func detailOutput( _ req: Request, _ model: DatabaseModel ) async throws -> DetailObject func detailApi( _ req: Request ) async throws -> DetailObject func setupDetailRoutes( _ routes: RoutesBuilder ) } extension ApiDetailController { func detailApi( _ req: Request ) async throws -> DetailObject { let model = try await findBy(identifier(req), on: req.db) return try await detailOutput(req, model) } func setupDetailRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.get(use: detailApi) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Api/Controllers/ApiListController.swift ================================================ import Vapor protocol ApiListController: ListController { associatedtype ListObject: Content func listOutput( _ req: Request, _ models: [DatabaseModel] ) async throws -> [ListObject] func listApi( _ req: Request ) async throws -> [ListObject] func setupListRoutes( _ routes: RoutesBuilder ) } extension ApiListController { func listApi( _ req: Request ) async throws -> [ListObject] { let models = try await list(req) return try await listOutput(req, models) } func setupListRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) baseRoutes.get(use: listApi) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Api/Controllers/ApiPatchController.swift ================================================ import Vapor public protocol ApiPatchController: PatchController { associatedtype PatchObject: Decodable func patchInput( _ req: Request, _ model: DatabaseModel, _ input: PatchObject ) async throws func patchApi( _ req: Request ) async throws -> Response func patchResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response func setupPatchRoutes( _ routes: RoutesBuilder ) } public extension ApiPatchController { func patchApi( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let input = try req.content.decode(PatchObject.self) try await patchInput(req, model, input) try await model.update(on: req.db) return try await patchResponse(req, model) } func setupPatchRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.patch(use: patchApi) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Api/Controllers/ApiUpdateController.swift ================================================ import Vapor public protocol ApiUpdateController: UpdateController { associatedtype UpdateObject: Decodable func updateInput( _ req: Request, _ model: DatabaseModel, _ input: UpdateObject ) async throws func updateApi( _ req: Request ) async throws -> Response func updateResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response func setupUpdateRoutes( _ routes: RoutesBuilder ) } public extension ApiUpdateController { func updateApi( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let input = try req.content.decode(UpdateObject.self) try await updateInput(req, model, input) try await model.update(on: req.db) return try await updateResponse(req, model) } func setupUpdateRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.put(use: updateApi) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/BlogModule.swift ================================================ import Vapor struct BlogModule: ModuleInterface { let router = BlogRouter() func boot(_ app: Application) throws { app.migrations.add(BlogMigrations.v1()) app.migrations.add(BlogMigrations.seed()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/BlogRouter.swift ================================================ import Vapor struct BlogRouter: RouteCollection { let frontendController = BlogFrontendController() let postAdminController = BlogPostAdminController() let postApiController = BlogPostApiController() let categoryAdminController = BlogCategoryAdminController() let categoryApiController = BlogCategoryApiController() func boot(routes: RoutesBuilder) throws { routes.get("blog", use: frontendController.blogView) routes.get(.anything, use: frontendController.postView) let admin = routes .grouped( AuthenticatedUser.redirectMiddleware( path: "/" ) ) .grouped("admin") postAdminController.setupRoutes(admin) categoryAdminController.setupRoutes(admin) let api = routes.grouped("api") postApiController.setupRoutes(api) categoryApiController.setupRoutes(api) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/Controllers/BlogCategoryAdminController.swift ================================================ import Vapor import Fluent struct BlogCategoryAdminController: AdminController { typealias ApiModel = Blog.Category typealias DatabaseModel = BlogCategoryModel typealias CreateModelEditor = BlogCategoryEditor typealias UpdateModelEditor = BlogCategoryEditor func listColumns() -> [ColumnContext] { [ .init("title"), ] } func listCells( for model: DatabaseModel ) -> [CellContext] { [ .init( model.title, link: .init(label: model.title) ), ] } func detailFields( for model: DatabaseModel ) -> [DetailContext] { [ .init("title", model.title), ] } func deleteInfo( _ model: DatabaseModel ) -> String { model.title } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/Controllers/BlogCategoryApiController.swift ================================================ import Vapor extension Blog.Category.List: Content {} extension Blog.Category.Detail: Content {} struct BlogCategoryApiController: ApiController { typealias ApiModel = Blog.Category typealias DatabaseModel = BlogCategoryModel func listOutput( _ req: Request, _ models: [BlogCategoryModel] ) async throws -> [Blog.Category.List] { models.map { .init(id: $0.id!, title: $0.title) } } func detailOutput( _ req: Request, _ model: BlogCategoryModel ) async throws -> Blog.Category.Detail { .init( id: model.id!, title: model.title ) } func createInput( _ req: Request, _ model: BlogCategoryModel, _ input: Blog.Category.Create ) async throws { model.title = input.title } func updateInput( _ req: Request, _ model: BlogCategoryModel, _ input: Blog.Category.Update ) async throws { model.title = input.title } func patchInput( _ req: Request, _ model: BlogCategoryModel, _ input: Blog.Category.Patch ) async throws { model.title = input.title ?? model.title } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift ================================================ import Vapor import Fluent struct BlogFrontendController { func blogView(req: Request) async throws -> Response { let posts = try await BlogPostModel .query(on: req.db) .sort(\.$date, .descending) .all() let api = BlogPostApiController() let listOutput = try await api.listOutput(req, posts) let ctx = BlogPostsContext( icon: "🔥", title: "Blog", message: "Hot news and stories about everything.", posts: listOutput ) return req.templates.renderHtml(BlogPostsTemplate(ctx)) } func postView(req: Request) async throws -> Response { let slug = req.url.path.trimmingCharacters( in: .init(charactersIn: "/") ) guard let post = try await BlogPostModel .query(on: req.db) .filter(\.$slug == slug) .with(\.$category) .first() else { return req.redirect(to: "/") } let api = BlogPostApiController() let postOutput = try await api.detailOutput(req, post) let ctx = BlogPostContext(post: postOutput) return req.templates.renderHtml(BlogPostTemplate(ctx)) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/Controllers/BlogPostAdminController.swift ================================================ import Vapor import Fluent struct BlogPostAdminController: AdminController { typealias ApiModel = Blog.Post typealias DatabaseModel = BlogPostModel typealias CreateModelEditor = BlogPostEditor typealias UpdateModelEditor = BlogPostEditor func listColumns() -> [ColumnContext] { [ .init("image"), .init("title"), ] } func listCells(for model: DatabaseModel) -> [CellContext] { [ .init(model.imageKey, type: .image), .init(model.title, link: .init(label: model.title)), ] } func detailFields(for model: DatabaseModel) -> [DetailContext] { [ .init("image", model.imageKey, type: .image), .init("title", model.title), ] } func deleteInfo( _ model: DatabaseModel ) -> String { model.title } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/Controllers/BlogPostApiController.swift ================================================ import Vapor extension Blog.Post.List: Content {} extension Blog.Post.Detail: Content {} struct BlogPostApiController: ApiController { typealias ApiModel = Blog.Post typealias DatabaseModel = BlogPostModel func listOutput( _ req: Request, _ models: [DatabaseModel] ) async throws -> [Blog.Post.List] { models.map { model in .init( id: model.id!, title: model.title, slug: model.slug, image: model.imageKey, excerpt: model.excerpt, date: model.date ) } } func detailOutput( _ req: Request, _ model: DatabaseModel ) async throws -> Blog.Post.Detail { .init( id: model.id!, title: model.title, slug: model.slug, image: model.imageKey, excerpt: model.excerpt, date: model.date, category: .init( id: model.category.id!, title: model.category.title ), content: model.content ) } func createInput( _ req: Request, _ model: DatabaseModel, _ input: Blog.Post.Create ) async throws { model.title = input.title model.slug = input.slug model.imageKey = input.image model.excerpt = input.excerpt model.date = input.date model.content = input.content } func updateInput( _ req: Request, _ model: DatabaseModel, _ input: Blog.Post.Update ) async throws { model.title = input.title model.slug = input.slug model.imageKey = input.image model.excerpt = input.excerpt model.date = input.date model.content = input.content } func patchInput( _ req: Request, _ model: DatabaseModel, _ input: Blog.Post.Patch ) async throws { model.title = input.title ?? model.title model.slug = input.slug ?? model.slug model.imageKey = input.image ?? model.imageKey model.excerpt = input.excerpt ?? model.excerpt model.date = input.date ?? model.date model.content = input.content ?? model.content } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/Database/Migrations/BlogMigrations.swift ================================================ import Foundation import Fluent enum BlogMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema) .id() .field(BlogCategoryModel.FieldKeys.v1.title, .string, .required) .create() try await db.schema(BlogPostModel.schema) .id() .field(BlogPostModel.FieldKeys.v1.title, .string, .required) .field(BlogPostModel.FieldKeys.v1.slug, .string, .required) .field(BlogPostModel.FieldKeys.v1.imageKey, .string, .required) .field(BlogPostModel.FieldKeys.v1.excerpt, .data, .required) .field(BlogPostModel.FieldKeys.v1.date, .datetime, .required) .field(BlogPostModel.FieldKeys.v1.content, .data, .required) .field(BlogPostModel.FieldKeys.v1.categoryId, .uuid) .foreignKey( BlogPostModel.FieldKeys.v1.categoryId, references: BlogCategoryModel.schema, .id, onDelete: DatabaseSchema.ForeignKeyAction.setNull, onUpdate: .cascade ) .unique(on: BlogPostModel.FieldKeys.v1.slug) .create() } func revert(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema).delete() try await db.schema(BlogPostModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let categories = (1...4).map { index in BlogCategoryModel(title: "Sample category #\(index)") } try await categories.create(on: db) try await (1...9).map { index in BlogPostModel( id: nil, title: "Sample post #\(index)", slug: "sample-post-\(index)", imageKey: "/img/posts/\(String(format: "%02d", index + 1)).jpg", excerpt: "Lorem ipsum", date: Date().addingTimeInterval(-Double.random(in: 0...(86400 * 60))), content: "Lorem ipsum dolor sit amet.", categoryId: categories[Int.random(in: 0.. [FormComponent] { ImageField("image", path: "blog/post") .read { [unowned self] in $1.output.context.previewUrl = model.imageKey ($1 as! ImageField).imageKey = model.imageKey } .write { [unowned self] in model.imageKey = ($1 as! ImageField).imageKey ?? "" } InputField("slug") .config { $0.output.context.label.required = true } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = model.slug } .write { [unowned self] in model.slug = $1.input } InputField("title") .config { $0.output.context.label.required = true } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = model.title } .write { [unowned self] in model.title = $1.input } InputField("date") .config { $0.output.context.label.required = true $0.output.context.value = dateFormatter.string(from: Date()) } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = dateFormatter.string(from: model.date) } .write { [unowned self] in model.date = dateFormatter.date(from: $1.input) ?? Date() } TextareaField("excerpt") .read { [unowned self] in $1.output.context.value = model.excerpt } .write { [unowned self] in model.excerpt = $1.input } TextareaField("content") .read { [unowned self] in $1.output.context.value = model.content } .write { [unowned self] in model.content = $1.input } SelectField("category") .load { req, field in let categories = try await BlogCategoryModel .query(on: req.db) .all() field.output.context.options = categories.map { OptionContext(key: $0.id!.uuidString, label: $0.title) } } .read { [unowned self] req, field in field.output.context.value = model.$category.id.uuidString } .write { [unowned self] req, field in if let uuid = UUID(uuidString: field.input), let category = try await BlogCategoryModel .find(uuid, on: req.db) { model.$category.id = category.id! } } } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/Objects/Blog.swift ================================================ enum Blog: ApiModuleInterface { enum Post: ApiModelInterface { typealias Module = Blog } enum Category: ApiModelInterface { typealias Module = Blog static let pathKey: String = "categories" } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/Objects/BlogCategory.swift ================================================ import Foundation extension Blog.Category { struct List: Codable { let id: UUID let title: String } struct Detail: Codable { let id: UUID let title: String } struct Create: Codable { let title: String } struct Update: Codable { let title: String } struct Patch: Codable { let title: String? } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/Objects/BlogPost.swift ================================================ import Foundation extension Blog.Post { struct List: Codable { let id: UUID let title: String let slug: String let image: String let excerpt: String let date: Date } struct Detail: Codable { let id: UUID let title: String let slug: String let image: String let excerpt: String let date: Date let category: Blog.Category.List let content: String } struct Create: Codable { let title: String let slug: String let image: String let excerpt: String let date: Date let content: String } struct Update: Codable { let title: String let slug: String let image: String let excerpt: String let date: Date let content: String } struct Patch: Codable { let title: String? let slug: String? let image: String? let excerpt: String? let date: Date? let content: String? } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminDeleteContext.swift ================================================ struct BlogPostAdminDeleteContext { let title: String let name: String let type: String } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminDetailContext.swift ================================================ struct BlogPostAdminDetailContext { let title: String let detail: Blog.Post.Detail } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminEditContext.swift ================================================ struct BlogPostAdminEditContext { let title: String let form: TemplateRepresentable } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminListContext.swift ================================================ struct BlogPostAdminListContext { let title: String let list: [Blog.Post.List] } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostContext.swift ================================================ struct BlogPostContext { let post: Blog.Post.Detail } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostsContext.swift ================================================ struct BlogPostsContext { let icon: String let title: String let message: String let posts: [Blog.Post.List] } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminDeleteTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminDeleteTemplate: TemplateRepresentable { var context: BlogPostAdminDeleteContext init( _ context: BlogPostAdminDeleteContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Span("🗑") .class("icon") H1(context.title) P("You are about to permanently delete the
`\(context.name)` \(context.type).") Form { Input() .type(.submit) .class(["button", "destructive"]) .style("display: inline") .value("Delete") A("Cancel") .href("/admin/blog/posts/") .class(["button", "cancel"]) } .method(.post) .id("delete-form") } .class(["lead", "container", "center"]) } .render(req) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminDetailTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminDetailTemplate: TemplateRepresentable { var context: BlogPostAdminDetailContext init( _ context: BlogPostAdminDetailContext ) { self.context = context } var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .short return formatter }() @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") Dl { Dt("Image") Dd { Img( src: context.detail.image, alt: context.detail.title ) } Dt("Title") Dd(context.detail.title) Dt("Excerpt") Dd(context.detail.excerpt) Dt("Date") Dd(dateFormatter.string(from: context.detail.date)) Dt("Content") Dd(context.detail.content) } } .id("detail") .class("container") } .render(req) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminEditTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminEditTemplate: TemplateRepresentable { var context: BlogPostAdminEditContext init( _ context: BlogPostAdminEditContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") context.form.render(req) } .id("edit") .class("container") } .render(req) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminListTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminListTemplate: TemplateRepresentable { var context: BlogPostAdminListContext init( _ context: BlogPostAdminListContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") Table { Thead { Tr { Th("Image") Th("Title") Th("Preview") } } Tbody { for item in context.list { Tr { Td { Img(src: item.image, alt: item.title) } Td { A(item.title) .href("/admin/blog/posts/" + item.id.uuidString + "/") } Td { A("Preview") .href("/" + item.slug + "/") } } } } } } .id("list") } .render(req) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostTemplate: TemplateRepresentable { var context: BlogPostContext init( _ context: BlogPostContext ) { self.context = context } var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .short return formatter }() @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.post.title) ) { Div { Section { P(dateFormatter.string(from: context.post.date)) H1(context.post.title) P(context.post.excerpt) } .class(["lead", "container"]) Img(src: context.post.image, alt: context.post.title) Article { Text(context.post.content) } .class("container") } .id("post") } .render(req) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostsTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostsTemplate: TemplateRepresentable { var context: BlogPostsContext init( _ context: BlogPostsContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") Div { for post in context.posts { Article { A { Img(src: post.image, alt: post.title) H2(post.title) P(post.excerpt) } .href("/\(post.slug)/") } } } .class("grid-221") } .id("blog") } .render(req) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/User/Authenticators/UserCredentialsAuthenticator.swift ================================================ import Vapor import Fluent struct UserCredentialsAuthenticator: AsyncCredentialsAuthenticator { struct Credentials: Content { let email: String let password: String } func authenticate( credentials: Credentials, for req: Request ) async throws { guard let user = try await UserAccountModel .query(on: req.db) .filter(\.$email == credentials.email) .first() else { return } do { guard try Bcrypt.verify( credentials.password, created: user.password ) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } catch { // do nothing... } } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/User/Authenticators/UserSessionAuthenticator.swift ================================================ import Vapor import Fluent struct UserSessionAuthenticator: AsyncSessionAuthenticator { typealias User = AuthenticatedUser func authenticate( sessionID: User.SessionID, for req: Request ) async throws { guard let user = try await UserAccountModel .find(sessionID, on: req.db) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/User/Controllers/UserFrontendController.swift ================================================ import Vapor struct UserFrontendController { private func renderSignInView( _ req: Request, _ form: UserLoginForm ) -> Response { let template = UserLoginTemplate( .init( icon: "⬇️", title: "Sign in", message: "Please log in with your existing account", form: form.render(req: req) ) ) return req.templates.renderHtml(template) } func signInView( _ req: Request ) async throws -> Response { renderSignInView(req, .init()) } func signInAction( _ req: Request ) async throws -> Response { /// the user is authenticated, we can store the user data inside the session too if let user = req.auth.get(AuthenticatedUser.self) { req.session.authenticate(user) return req.redirect(to: "/") } let form = UserLoginForm() try await form.process(req: req) let isValid = try await form.validate(req: req) if !isValid { form.error = "Invalid email or password." } return renderSignInView(req, form) } func signOut( _ req: Request ) throws -> Response { req.auth.logout(AuthenticatedUser.self) req.session.unauthenticate(AuthenticatedUser.self) return req.redirect(to: "/") } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/User/Database/Migrations/UserMigrations.swift ================================================ import Vapor import Fluent enum UserMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(UserAccountModel.schema) .id() .field(UserAccountModel.FieldKeys.v1.email, .string, .required) .field(UserAccountModel.FieldKeys.v1.password, .string, .required) .unique(on: UserAccountModel.FieldKeys.v1.email) .create() } func revert(on db: Database) async throws { try await db.schema(UserAccountModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let email = "root@localhost.com" let password = "ChangeMe1" let user = UserAccountModel( email: email, password: try Bcrypt.hash(password) ) try await user.create(on: db) } func revert(on db: Database) async throws { try await UserAccountModel.query(on: db).delete() } } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/User/Database/Models/UserAccountModel.swift ================================================ import Vapor import Fluent final class UserAccountModel: DatabaseModelInterface { typealias Module = UserModule struct FieldKeys { struct v1 { static var email: FieldKey { "email" } static var password: FieldKey { "password" } } } @ID() var id: UUID? @Field(key: FieldKeys.v1.email) var email: String @Field(key: FieldKeys.v1.password) var password: String init() { } init( id: UUID? = nil, email: String, password: String ) { self.id = id self.email = email self.password = password } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/User/Forms/UserLoginForm.swift ================================================ import Vapor final class UserLoginForm: AbstractForm { public convenience init() { self.init( action: .init( method: .post, url: "/sign-in/" ), submit: "Sign in" ) self.fields = createFields() } @FormComponentBuilder func createFields() -> [FormComponent] { InputField("email") .config { $0.output.context.label.required = true $0.output.context.type = .email } .validators { FormFieldValidator.required($1) FormFieldValidator.email($1) } InputField("password") .config { $0.output.context.label.required = true $0.output.context.type = .password } .validators { FormFieldValidator.required($1) } } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/User/Templates/Contexts/UserLoginContext.swift ================================================ struct UserLoginContext { let icon: String let title: String let message: String let form: TemplateRepresentable init( icon: String, title: String, message: String, form: TemplateRepresentable ) { self.icon = icon self.title = title self.message = message self.form = form } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/User/Templates/Html/UserLoginTemplate.swift ================================================ import Vapor import SwiftHtml struct UserLoginTemplate: TemplateRepresentable { var context: UserLoginContext init( _ context: UserLoginContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") context.form.render(req) } .id("user-login") .class("container") } .render(req) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/User/UserModule.swift ================================================ import Vapor struct UserModule: ModuleInterface { let router = UserRouter() func boot(_ app: Application) throws { app.migrations.add(UserMigrations.v1()) app.migrations.add(UserMigrations.seed()) app.middleware.use(UserSessionAuthenticator()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/User/UserRouter.swift ================================================ import Vapor struct UserRouter: RouteCollection { let frontendController = UserFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get("sign-in", use: frontendController.signInView) routes .grouped( UserCredentialsAuthenticator() ) .post("sign-in", use: frontendController.signInAction) routes.get("sign-out", use: frontendController.signOut) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Web/Controllers/WebFrontendController.swift ================================================ import Vapor struct WebFrontendController { func homeView(req: Request) throws -> Response { let ctx = WebHomeContext( icon: "👋", title: "Home", message: "Hi there, welcome to my page.", paragraphs: [ "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.", "Nisi ut aliquip ex ea commodo consequat.", ], link: .init( label: "Read my blog →", url: "/blog/" ) ) return req.templates.renderHtml( WebHomeTemplate(ctx) ) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Web/Templates/Contexts/WebHomeContext.swift ================================================ struct WebHomeContext { let icon: String let title: String let message: String let paragraphs: [String] let link: WebLinkContext } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Web/Templates/Contexts/WebIndexContext.swift ================================================ public struct WebIndexContext { public let title: String public init( title: String ) { self.title = title } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Web/Templates/Contexts/WebLinkContext.swift ================================================ public struct WebLinkContext { public let label: String public let url: String public init( label: String, url: String ) { self.label = label self.url = url } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Web/Templates/Html/WebHomeTemplate.swift ================================================ import Vapor import SwiftHtml struct WebHomeTemplate: TemplateRepresentable { var context: WebHomeContext init( _ context: WebHomeContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") for paragraph in context.paragraphs { P(paragraph) } WebLinkTemplate(context.link).render(req) } .id("home") .class("container") } .render(req) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg public struct WebIndexTemplate: TemplateRepresentable { public var context: WebIndexContext var body: Tag public init( _ context: WebIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Link(rel: .shortcutIcon) .href("/images/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css") Link(rel: .stylesheet) .href("/css/web.css") Title(context.title) } Body { Header { Div { A { Img(src: "/img/logo.png", alt: "Logo") } .id("site-logo") .href("/") Nav { Input() .type(.checkbox) .id("primary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("primary-menu-button") Div { A("Home") .href("/") .class("selected", req.url.path == "/") A("Blog") .href("/blog/") .class("selected", req.url.path == "/blog/") A("About") .href("#") .onClick("javascript:about();") if req.auth.has(AuthenticatedUser.self) { A("Admin") .href("/admin/") A("Sign out") .href("/sign-out/") } else { A("Sign in") .href("/sign-in/") } } .class("menu-items") } .id("primary-menu") } .id("navigation") } Main { body } Footer { Section { P { Text("This site is powered by ") A("Swift") .href("https://swift.org") .target(.blank) Text(" & ") A("Vapor") .href("https://vapor.codes") .target(.blank) Text(".") } P("myPage © 2020-2022") } } Script() .type(.javascript) .src("/js/web.js") } } .lang("en-US") } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Web/Templates/Html/WebLinkTemplate.swift ================================================ import Vapor import SwiftHtml struct WebLinkTemplate: TemplateRepresentable { var context: WebLinkContext init( _ context: WebLinkContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { A(context.label) .href(context.url) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Web/WebModule.swift ================================================ import Vapor struct WebModule: ModuleInterface { let router = WebRouter() func boot(_ app: Application) throws { try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Modules/Web/WebRouter.swift ================================================ import Vapor struct WebRouter: RouteCollection { let frontendController = WebFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get(use: frontendController.homeView) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Template/Request+Template.swift ================================================ import Vapor public extension Request { var templates: TemplateRenderer { .init(self) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Template/TemplateRenderer.swift ================================================ import Vapor import SwiftHtml public struct TemplateRenderer { var req: Request init(_ req: Request) { self.req = req } public func renderHtml( _ template: TemplateRepresentable, minify: Bool = false, indent: Int = 4 ) -> Response { let doc = Document(.html) { template.render(req) } let body = DocumentRenderer( minify: minify, indent: indent ) .render(doc) return Response( status: .ok, headers: [ "content-type": "text/html" ], body: .init(string: body) ) } } ================================================ FILE: Chapter 12/myProject/Sources/App/Template/TemplateRepresentable.swift ================================================ import Vapor import SwiftSgml public protocol TemplateRepresentable { @TagBuilder func render(_ req: Request) -> Tag } ================================================ FILE: Chapter 12/myProject/Sources/App/configure.swift ================================================ import Vapor import Fluent import FluentSQLiteDriver import Liquid import LiquidLocalDriver public func configure( _ app: Application ) throws { app.fileStorages.use( .local( publicUrl: "http://localhost:8080", publicPath: app.directory.publicDirectory, workDirectory: "assets" ), as: .local ) app.routes.defaultMaxBodySize = "10mb" let dbPath = app.directory.resourcesDirectory + "db.sqlite" app.databases.use(.sqlite(.file(dbPath)), as: .sqlite) app.middleware.use( FileMiddleware( publicDirectory: app.directory.publicDirectory ) ) app.middleware.use(ExtendPathMiddleware()) app.sessions.use(.fluent) app.migrations.add(SessionRecord.migration) app.middleware.use(app.sessions.middleware) let modules: [ModuleInterface] = [ WebModule(), UserModule(), AdminModule(), BlogModule(), ] for module in modules { try module.boot(app) } try app.autoMigrate().wait() } ================================================ FILE: Chapter 12/myProject/Sources/App/routes.swift ================================================ import Vapor import SwiftHtml func routes(_ app: Application) throws { app.routes.get { req -> Response in req.templates.renderHtml( WebIndexTemplate( WebIndexContext( title: "Home" ) ) { P("Hi there, welcome to my page!") } ) } } ================================================ FILE: Chapter 12/myProject/Sources/Run/main.swift ================================================ import App import Vapor var env = try Environment.detect() try LoggingSystem.bootstrap(from: &env) let app = Application(env) defer { app.shutdown() } try configure(app) try app.run() ================================================ FILE: Chapter 12/myProject/Tests/AppTests/AppTests.swift ================================================ @testable import App import XCTVapor final class AppTests: XCTestCase { func testHelloWorld() throws { let app = Application(.testing) defer { app.shutdown() } try configure(app) try app.test(.GET, "hello", afterResponse: { res in XCTAssertEqual(res.status, .ok) XCTAssertEqual(res.body.string, "Hello, world!") }) } } ================================================ FILE: Chapter 12/myProject/docker-compose.yml ================================================ # 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 # Stop all: docker compose down # x-shared_environment: &shared_environment LOG_LEVEL: ${LOG_LEVEL:-debug} services: app: image: myproject:latest build: context: . environment: <<: *shared_environment 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"] ================================================ FILE: Chapter 13/myProject/Dockerfile ================================================ # ================================ # Build image # ================================ FROM swift:6.0-noble 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 \ $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true) # Copy entire repo into container COPY . . # Build the application, 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 \ --product myProject \ --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)/myProject" ./ # 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:noble # 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 ["./myProject"] CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] ================================================ FILE: Chapter 13/myProject/Package.swift ================================================ // swift-tools-version:5.7 import PackageDescription let package = Package( name: "myProject", platforms: [ .macOS(.v12) ], dependencies: [ .package( url: "https://github.com/vapor/vapor", from: "4.70.0" ), .package( url: "https://github.com/vapor/fluent", from: "4.4.0" ), .package( url: "https://github.com/vapor/fluent-sqlite-driver", from: "4.1.0" ), .package( url: "https://github.com/binarybirds/liquid", from: "1.3.0" ), .package( url: "https://github.com/binarybirds/liquid-local-driver", from: "1.3.0" ), .package( url: "https://github.com/binarybirds/swift-html", from: "1.7.0" ), ], targets: [ .target(name: "App", dependencies: [ .product(name: "Vapor", package: "vapor"), .product(name: "Fluent", package: "fluent"), .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), .product(name: "Liquid", package: "liquid"), .product(name: "LiquidLocalDriver", package: "liquid-local-driver"), .product(name: "SwiftHtml", package: "swift-html"), .product(name: "SwiftSvg", package: "swift-html"), ]), .executableTarget(name: "Run", dependencies: ["App"]), .testTarget(name: "AppTests", dependencies: [ .target(name: "App"), .product(name: "XCTVapor", package: "vapor"), ]) ] ) ================================================ FILE: Chapter 13/myProject/Public/css/admin.css ================================================ tr { column-gap: 1rem; } ================================================ FILE: Chapter 13/myProject/Public/css/web.css ================================================ #blog h2 { margin: 0.5rem 0; } ================================================ FILE: Chapter 13/myProject/Public/js/admin.js ================================================ document.addEventListener("keydown", function(e) { if ( (window.navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey) && e.keyCode == 83 ) { e.preventDefault(); document.forms[0].submit(); } }, false); ================================================ FILE: Chapter 13/myProject/Public/js/web.js ================================================ function about() { alert("myPage\n\nversion 1.0.0"); } ================================================ FILE: Chapter 13/myProject/Sources/App/Extensions/Svg+MenuIcon.swift ================================================ import SwiftSvg extension Svg { static func menuIcon() -> Svg { Svg { Line(x1: 3, y1: 12, x2: 21, y2: 12) Line(x1: 3, y1: 6, x2: 21, y2: 6) Line(x1: 3, y1: 18, x2: 21, y2: 18) } .width(24) .height(24) .viewBox(minX: 0, minY: 0, width: 24, height: 24) .fill("none") .stroke("currentColor") .strokeWidth(2) .strokeLinecap("round") .strokeLinejoin("round") } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/ApiModelInterface.swift ================================================ import Vapor public protocol ApiModelInterface { associatedtype Module: ApiModuleInterface static var pathKey: String { get } static var pathIdKey: String { get } } extension ApiModelInterface { static var pathKey: String { String(describing: self).lowercased() + "s" } static var pathIdKey: String { String(describing: self).lowercased() + "Id" } static var pathIdComponent: PathComponent { .init(stringLiteral: ":" + pathIdKey) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/ApiModuleInterface.swift ================================================ import Foundation public protocol ApiModuleInterface { static var pathKey: String { get } } public extension ApiModuleInterface { static var pathKey: String { String(describing: self).lowercased() } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/AuthenticatedUser.swift ================================================ import Vapor public struct AuthenticatedUser { public let id: UUID public let email: String public init( id: UUID, email: String ) { self.id = id self.email = email } } extension AuthenticatedUser: SessionAuthenticatable { public var sessionID: UUID { id } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Controllers/CreateController.swift ================================================ import Vapor protocol CreateController: ModelController { func create( _ req: Request, _ model: DatabaseModel ) async throws func beforeCreate( _ req: Request, _ model: DatabaseModel ) async throws func afterCreate( _ req: Request, _ model: DatabaseModel ) async throws } extension CreateController { func create( _ req: Request, _ model: DatabaseModel ) async throws { try await beforeCreate(req, model) try await model.create(on: req.db) try await afterCreate(req, model) } func beforeCreate( _ req: Request, _ model: DatabaseModel ) async throws {} func afterCreate( _ req: Request, _ model: DatabaseModel ) async throws {} } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Controllers/DeleteController.swift ================================================ import Vapor public protocol DeleteController: ModelController { func delete( _ req: Request, _ model: DatabaseModel ) async throws func beforeDelete( _ req: Request, _ model: DatabaseModel ) async throws func afterDelete( _ req: Request, _ model: DatabaseModel ) async throws } public extension DeleteController { func delete( _ req: Request, _ model: DatabaseModel ) async throws { try await beforeDelete(req, model) try await model.delete(on: req.db) try await afterDelete(req, model) } func beforeDelete( _ req: Request, _ model: DatabaseModel ) async throws {} func afterDelete( _ req: Request, _ model: DatabaseModel ) async throws {} } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Controllers/DetailController.swift ================================================ import Vapor import Fluent protocol DetailController: ModelController { func detail( _ req: Request ) async throws -> DatabaseModel func beforeDetail( _ req: Request, _ queryBuilder: QueryBuilder ) async throws -> QueryBuilder func afterDetail( _ req: Request, _ model: DatabaseModel ) async throws -> DatabaseModel } extension DetailController { func detail( _ req: Request ) async throws -> DatabaseModel { let queryBuilder = DatabaseModel.query(on: req.db) let model = try await beforeDetail(req, queryBuilder) .filter(\._$id == identifier(req)) .first() guard let model = model else { throw Abort(.notFound) } return try await afterDetail(req, model) } func beforeDetail( _ req: Request, _ queryBuilder: QueryBuilder ) async throws -> QueryBuilder { queryBuilder } func afterDetail( _ req: Request, _ model: DatabaseModel ) async throws -> DatabaseModel { model } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Controllers/ListController.swift ================================================ import Vapor import Fluent protocol ListController: ModelController { func list( _ req: Request ) async throws -> [DatabaseModel] func beforeList( _ req: Request, _ queryBuilder: QueryBuilder ) async throws -> QueryBuilder func afterList( _ req: Request, _ models: [DatabaseModel] ) async throws -> [DatabaseModel] } extension ListController { func list( _ req: Request ) async throws -> [DatabaseModel] { try await DatabaseModel .query(on: req.db) .all() } func beforeList( _ req: Request, _ queryBuilder: QueryBuilder ) async throws -> QueryBuilder { queryBuilder } func afterList( _ req: Request, _ models: [DatabaseModel] ) async throws -> [DatabaseModel] { models } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Controllers/ModelController.swift ================================================ import Vapor import Fluent public struct Name { let singular: String let plural: String init( singular: String, plural: String? = nil ) { self.singular = singular self.plural = plural ?? singular + "s" } } public protocol ModelController { associatedtype ApiModel: ApiModelInterface associatedtype DatabaseModel: DatabaseModelInterface static var moduleName: String { get } static var modelName: Name { get } func identifier( _ req: Request ) throws -> UUID func findBy( _ id: UUID, on: Database ) async throws -> DatabaseModel func getBaseRoutes( _ routes: RoutesBuilder ) -> RoutesBuilder } extension ModelController { static var moduleName: String { DatabaseModel.Module.identifier.capitalized } static var modelName: Name { .init(singular: String(DatabaseModel.identifier.dropLast(1))) } func identifier( _ req: Request ) throws -> UUID { guard let id = req.parameters.get(ApiModel.pathIdKey), let uuid = UUID(uuidString: id) else { throw Abort(.badRequest) } return uuid } func findBy( _ id: UUID, on db: Database ) async throws -> DatabaseModel { guard let model = try await DatabaseModel.find(id, on: db) else { throw Abort(.notFound) } return model } func getBaseRoutes( _ routes: RoutesBuilder ) -> RoutesBuilder { routes .grouped(ApiModel.Module.pathKey.pathComponents) .grouped(ApiModel.pathKey.pathComponents) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Controllers/PatchController.swift ================================================ import Vapor public protocol PatchController: ModelController { func patch( _ req: Request, _ model: DatabaseModel ) async throws func beforePatch( _ req: Request, _ model: DatabaseModel ) async throws func afterPatch( _ req: Request, _ model: DatabaseModel ) async throws } public extension PatchController { func patch( _ req: Request, _ model: DatabaseModel ) async throws { try await beforePatch(req, model) try await model.update(on: req.db) try await afterPatch(req, model) } func beforePatch( _ req: Request, _ model: DatabaseModel ) async throws {} func afterPatch( _ req: Request, _ model: DatabaseModel ) async throws {} } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Controllers/UpdateController.swift ================================================ import Vapor public protocol UpdateController: ModelController { func update( _ req: Request, _ model: DatabaseModel ) async throws func beforeUpdate( _ req: Request, _ model: DatabaseModel ) async throws func afterUpdate( _ req: Request, _ model: DatabaseModel ) async throws } public extension UpdateController { func update( _ req: Request, _ model: DatabaseModel ) async throws { try await beforeUpdate(req, model) try await model.update(on: req.db) try await afterUpdate(req, model) } func beforeUpdate( _ req: Request, _ model: DatabaseModel ) async throws {} func afterUpdate( _ req: Request, _ model: DatabaseModel ) async throws {} } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/DatabaseModelInterface.swift ================================================ import Vapor import Fluent public protocol DatabaseModelInterface: Fluent.Model where Self.IDValue == UUID { associatedtype Module: ModuleInterface static var identifier: String { get } } public extension DatabaseModelInterface { static var schema: String { Module.identifier + "_" + identifier } static var identifier: String { String(describing: self) .dropFirst(Module.identifier.count) .dropLast(5) .lowercased() + "s" } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Extensions/ByteBuffer+Data.swift ================================================ import Vapor public extension ByteBuffer { var data: Data? { getData(at: 0, length: readableBytes) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Extensions/File+ByteBuffer.swift ================================================ import Vapor public extension File { var byteBuffer: ByteBuffer { data } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Form/AbstractForm.swift ================================================ import Vapor open class AbstractForm: FormComponent { open var action: FormAction open var fields: [FormComponent] open var error: String? open var submit: String? public init( action: FormAction = .init(), fields: [FormComponent] = [], error: String? = nil, submit: String? = nil ) { self.action = action self.fields = fields self.error = error self.submit = submit self.action.enctype = .multipart } open func load(req: Request) async throws { for field in fields { try await field.load(req: req) } } open func process(req: Request) async throws { for field in fields { try await field.process(req: req) } } open func validate(req: Request) async throws -> Bool { var result: [Bool] = [] for field in fields { result.append(try await field.validate(req: req)) } return result.filter { $0 == false }.isEmpty } open func write(req: Request) async throws { for field in fields { try await field.write(req: req) } } open func save(req: Request) async throws { for field in fields { try await field.save(req: req) } } open func read(req: Request) async throws { for field in fields { try await field.read(req: req) } } open func render(req: Request) -> TemplateRepresentable { FormTemplate(getContext(req)) } func getContext(_ req: Request) -> FormContext { .init( action: action, fields: fields.map { $0.render(req: req)}, error: error, submit: submit ) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Form/AbstractFormField.swift ================================================ import Vapor open class AbstractFormField< Input: Decodable, Output: TemplateRepresentable >: FormComponent { public var key: String public var input: Input public var output: Output public var error: String? // MARK: - event blocks public typealias FormFieldBlock = (Request, AbstractFormField) async throws -> Void public typealias FormFieldValidatorsBlock = ((Request, AbstractFormField) -> [AsyncValidator]) private var readBlock: FormFieldBlock? private var writeBlock: FormFieldBlock? private var loadBlock: FormFieldBlock? private var saveBlock: FormFieldBlock? private var validatorsBlock: FormFieldValidatorsBlock? // MARK: - init & config public init( key: String, input: Input, output: Output, error: String? = nil ) { self.key = key self.input = input self.output = output self.error = error } open func config( _ block: (AbstractFormField) -> Void ) -> Self { block(self) return self } // MARK: - Block setters open func read(_ block: @escaping FormFieldBlock) -> Self { readBlock = block return self } open func write(_ block: @escaping FormFieldBlock) -> Self { writeBlock = block return self } open func load(_ block: @escaping FormFieldBlock) -> Self { loadBlock = block return self } open func save(_ block: @escaping FormFieldBlock) -> Self { saveBlock = block return self } open func validators( @AsyncValidatorBuilder _ block: @escaping FormFieldValidatorsBlock ) -> Self { validatorsBlock = block return self } // MARK: - FormComponent open func process(req: Request) async throws { if let value = try? req.content.get(Input.self, at: key) { input = value } } open func validate(req: Request) async throws -> Bool { guard let validators = validatorsBlock else { return true } return await RequestValidator(validators(req, self)).isValid(req) } open func read(req: Request) async throws { try await readBlock?(req, self) } open func write(req: Request) async throws { try await writeBlock?(req, self) } open func load(req: Request) async throws { try await loadBlock?(req, self) } open func save(req: Request) async throws { try await saveBlock?(req, self) } open func render(req: Request) -> TemplateRepresentable { output } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Form/Fields/HiddenField.swift ================================================ import Vapor public final class HiddenField: AbstractFormField< String, HiddenFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Form/Fields/ImageField.swift ================================================ import Vapor public final class ImageField: AbstractFormField< FormImageInput, ImageFieldTemplate > { public var imageKey: String? { didSet { output.context.data.originalKey = imageKey } } public var path: String public init( _ key: String, path: String ) { self.path = path super.init( key: key, input: .init(key: key), output: .init(.init(key: key)) ) } public override func process( req: Request ) async throws { /// process input input.file = try? req.content.get( File.self, at: key ) input.data.originalKey = try? req.content.get( String.self, at: key + "OriginalKey" ) if let temporaryFileKey = try? req.content.get( String.self, at: key + "TemporaryFileKey" ), let temporaryFileName = try? req.content.get( String.self, at: key + "TemporaryFileName" ) { input.data.temporaryFile = .init( key: temporaryFileKey, name: temporaryFileName ) } input.data.shouldRemove = (try? req.content.get( Bool.self, at: key + "ShouldRemove" )) ?? false /// remove & upload file if input.data.shouldRemove { if let originalKey = input.data.originalKey { try? await req.fs.delete(key: originalKey) } } else if let file = input.file, let data = file.byteBuffer.data, !data.isEmpty { if let tmpKey = input.data.temporaryFile?.key { try? await req.fs.delete(key: tmpKey) } let key = "tmp/\(UUID().uuidString).tmp" _ = try await req.fs.upload(key: key, data: data) /// update the temporary image input.data.temporaryFile = .init( key: key, name: file.filename ) } /// update output values output.context.data = input.data } public override func write( req: Request ) async throws { imageKey = input.data.originalKey if input.data.shouldRemove { if let key = input.data.originalKey { try? await req.fs.delete(key: key) } imageKey = nil } else if let file = input.data.temporaryFile { var newKey = path + "/" + file.name if await req.fs.exists(key: newKey) { let formatter = DateFormatter() formatter.dateFormat="y-MM-dd-HH-mm-ss-" let prefix = formatter.string(from: .init()) newKey = path + "/" + prefix + file.name } _ = try await req.fs.move(key: file.key, to: newKey) input.data.temporaryFile = nil if let key = input.data.originalKey { try? await req.fs.delete(key: key) } imageKey = newKey } try await super.write(req: req) } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Form/Fields/InputField.swift ================================================ public final class InputField: AbstractFormField< String, InputFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init( key: key ) ) ) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Form/Fields/SelectField.swift ================================================ import Vapor public final class SelectField: AbstractFormField< String, SelectFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Form/Fields/TextareaField.swift ================================================ import Vapor public final class TextareaField: AbstractFormField< String, TextareaFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Form/FormAction.swift ================================================ import SwiftHtml public struct FormAction { public var method: SwiftHtml.Method public var url: String? public var enctype: SwiftHtml.Enctype? public init( method: SwiftHtml.Method = .post, url: String? = nil, enctype: SwiftHtml.Enctype? = nil ) { self.method = method self.url = url self.enctype = enctype } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Form/FormComponent.swift ================================================ import Vapor public protocol FormComponent { func load(req: Request) async throws func process(req: Request) async throws func validate(req: Request) async throws -> Bool func write(req: Request) async throws func save(req: Request) async throws func read(req: Request) async throws func render(req: Request) -> TemplateRepresentable } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Form/FormComponentBuilder.swift ================================================ @resultBuilder public enum FormComponentBuilder { public static func buildBlock( _ components: FormComponent... ) -> [FormComponent] { components } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Form/FormImageData.swift ================================================ public struct FormImageData: Codable { public struct TemporaryFile: Codable { public let key: String public let name: String public init( key: String, name: String ) { self.key = key self.name = name } } public var originalKey: String? public var temporaryFile: TemporaryFile? public var shouldRemove: Bool public init( originalKey: String? = nil, temporaryFile: TemporaryFile? = nil, shouldRemove: Bool = false ) { self.originalKey = originalKey self.temporaryFile = temporaryFile self.shouldRemove = shouldRemove } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Form/FormImageInput.swift ================================================ import Vapor public struct FormImageInput: Codable { public var key: String public var file: File? public var data: FormImageData public init( key: String, file: File? = nil, data: FormImageData? = nil ) { self.key = key self.file = file self.data = data ?? .init() } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/ModelEditorInterface.swift ================================================ import Vapor public protocol ModelEditorInterface: FormComponent { associatedtype Model: DatabaseModelInterface var model: Model { get } var form: AbstractForm { get } init(model: Model, form: AbstractForm) @FormComponentBuilder var formFields: [FormComponent] { get } } public extension ModelEditorInterface { func load(req: Request) async throws { try await form.load(req: req) } func process(req: Request) async throws { try await form.process(req: req) } func validate(req: Request) async throws -> Bool { try await form.validate(req: req) } func write(req: Request) async throws { try await form.write(req: req) } func save(req: Request) async throws { try await form.save(req: req) } func read(req: Request) async throws { try await form.read(req: req) } func render(req: Request) -> TemplateRepresentable { form.render(req: req) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/ModuleInterface.swift ================================================ import Vapor public protocol ModuleInterface { static var identifier: String { get } func boot(_ app: Application) throws } public extension ModuleInterface { func boot(_ app: Application) throws {} static var identifier: String { String(describing: self).dropLast(6).lowercased() } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Contexts/CellContext.swift ================================================ public struct CellContext { public enum `Type`: String { case text case image } public let value: String public let link: LinkContext? public let type: `Type` public init( _ value: String, link: LinkContext? = nil, type: `Type` = .text ) { self.type = type self.value = value self.link = link } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Contexts/ColumnContext.swift ================================================ public struct ColumnContext { public let key: String public let label: String public init( _ key: String, label: String? = nil ) { self.key = key self.label = label ?? key.capitalized } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Contexts/DetailContext.swift ================================================ public struct DetailContext { public enum `Type`: String { case text case image } public let key: String public let label: String public let value: String public let type: `Type` public init( _ key: String, _ value: String, label: String? = nil, type: `Type` = .text ) { self.key = key self.label = label ?? key.capitalized self.value = value self.type = type } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Contexts/FormContext.swift ================================================ public struct FormContext { public var action: FormAction public var fields: [TemplateRepresentable] public var error: String? public var submit: String? } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Contexts/HiddenFieldContext.swift ================================================ public struct HiddenFieldContext { public let key: String public var value: String? public init( key: String, value: String? = nil ) { self.key = key self.value = value } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Contexts/ImageFieldContext.swift ================================================ public struct ImageFieldContext { public let key: String public var label: LabelContext public var data: FormImageData public var previewUrl: String? public var accept: String? public var error: String? public init( key: String, label: LabelContext? = nil, data: FormImageData = .init(), previewUrl: String? = nil, accept: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.data = data self.previewUrl = previewUrl self.accept = accept self.error = error } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Contexts/InputFieldContext.swift ================================================ import SwiftHtml public struct InputFieldContext { public let key: String public var label: LabelContext public var type: SwiftHtml.Input.`Type` public var placeholder: String? public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, type: Input.`Type` = .text, placeholder: String? = nil, value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.type = type self.placeholder = placeholder self.value = value self.error = error } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Contexts/LabelContext.swift ================================================ public struct LabelContext { public var key: String public var title: String? public var required: Bool public var more: String? public init( key: String, title: String? = nil, required: Bool = false, more: String? = nil ) { self.key = key self.title = title self.required = required self.more = more } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Contexts/LinkContext.swift ================================================ import Vapor public struct LinkContext { public let label: String public let path: String public let absolute: Bool public let isBlank: Bool public let dropLast: Int public init( label: String, path: String = "", absolute: Bool = false, isBlank: Bool = false, dropLast: Int = 0 ) { self.label = label self.path = path self.absolute = absolute self.isBlank = isBlank self.dropLast = dropLast } public func url( _ req: Request, _ infix: [PathComponent] = [] ) -> String { if absolute { return path } return "/" + (req.url.path.pathComponents.dropLast(dropLast) + (infix + path.pathComponents)).string } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Contexts/OptionContext.swift ================================================ public struct OptionContext { public var key: String public var label: String public init( key: String, label: String ) { self.key = key self.label = label } } public extension OptionContext { static func yesNo() -> [OptionContext] { ["yes", "no"].map { .init(key: $0, label: $0.capitalized) } } static func trueFalse() -> [OptionContext] { [true, false].map { .init(key: String($0), label: String($0).capitalized) } } static func numbers( _ numbers: [Int] ) -> [OptionContext] { numbers.map { .init(key: String($0), label: String($0)) } } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Contexts/RowContext.swift ================================================ public struct RowContext { public let id: String public let cells: [CellContext] public init( id: String, cells: [CellContext] ) { self.id = id self.cells = cells } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Contexts/SelectFieldContext.swift ================================================ public struct SelectFieldContext { public let key: String public var label: LabelContext public var options: [OptionContext] public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, options: [OptionContext] = [], value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.options = options self.value = value self.error = error } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Contexts/TableContext.swift ================================================ public struct TableContext { public let columns: [ColumnContext] public let rows: [RowContext] public let actions: [LinkContext] public init( columns: [ColumnContext], rows: [RowContext], actions: [LinkContext] = [] ) { self.columns = columns self.rows = rows self.actions = actions } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Contexts/TextareaFieldContext.swift ================================================ public struct TextareaFieldContext { public let key: String public var label: LabelContext public var placeholder: String? public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, placeholder: String? = nil, value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.placeholder = placeholder self.value = value self.error = error } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Html/CellTemplate.swift ================================================ import Vapor import SwiftHtml public struct CellTemplate: TemplateRepresentable { var context: CellContext var rowId: String public init( _ context: CellContext, rowId: String ) { self.context = context self.rowId = rowId } @TagBuilder public func render( _ req: Request ) -> Tag { Td { switch context.type { case .text: if let link = context.link { LinkTemplate(link, pathInfix: rowId).render(req) } else { Text(context.value) } case .image: if let link = context.link { LinkTemplate(link, pathInfix: rowId) { label in Img(src: context.value, alt: label) } .render(req) } else { Img(src: context.value, alt: context.value) } } } .class("field") } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Html/DetailTemplate.swift ================================================ import Vapor import SwiftHtml public struct DetailTemplate: TemplateRepresentable { var context: DetailContext public init( _ context: DetailContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Dt(context.label) switch context.type { case .text: if context.value.isEmpty { Dd(" ") } else { Dd( context.value.replacingOccurrences( of: "\n", with: "
" ) ) } case .image: Dd { Img( src: context.value, alt: context.label ) } } } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Html/FormTemplate.swift ================================================ import Vapor import SwiftHtml public struct FormTemplate: TemplateRepresentable { var context: FormContext public init( _ context: FormContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Form { if let error = context.error { Section { P(error) .class("error") } } for field in context.fields { Section { field.render(req) } } Section { Input() .type(.submit) .value(context.submit ?? "Save") } } .method(context.action.method) .action(context.action.url) .enctype(context.action.enctype) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Html/HiddenFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct HiddenFieldTemplate: TemplateRepresentable { var context: HiddenFieldContext public init( _ context: HiddenFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Input() .type(.hidden) .name(context.key) .value(context.value) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Html/ImageFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct ImageFieldTemplate: TemplateRepresentable { public var context: ImageFieldContext public init( _ context: ImageFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { if let url = context.previewUrl { Img(src: req.fs.resolve(key: url), alt: context.key) } else { Img(src: "/img/logo.png", alt: "default post image") } LabelTemplate(context.label).render(req) Input() .type(.file) .key(context.key) .class("field") .accept(context.accept) if let temporaryFile = context.data.temporaryFile { Input() .key(context.key + "TemporaryFileKey") .value(temporaryFile.key) .type(.hidden) Input() .key(context.key + "TemporaryFileName") .value(temporaryFile.name) .type(.hidden) } if let key = context.data.originalKey { Input() .key(context.key + "OriginalKey") .value(key) .type(.hidden) } if !context.label.required { Input() .key(context.key + "ShouldRemove") .value(String(true)) .type(.checkbox) .checked(context.data.shouldRemove) Label("Remove") .for(context.key + "Remove") } if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Html/InputFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct InputFieldTemplate: TemplateRepresentable { public var context: InputFieldContext public init( _ context: InputFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Input() .type(context.type) .key(context.key) .placeholder(context.placeholder) .value(context.value) .class("field") if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Html/LabelTemplate.swift ================================================ import Vapor import SwiftHtml public struct LabelTemplate: TemplateRepresentable { var context: LabelContext public init( _ context: LabelContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Label { Text(context.title ?? context.key.capitalized) if let more = context.more { Span(more) .class("more") } if context.required { Span("*") .class("required") } }.for(context.key) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Html/LinkTemplate.swift ================================================ import Vapor import SwiftHtml public struct LinkTemplate: TemplateRepresentable { var context: LinkContext var body: Tag var pathInfix: String? public init( _ context: LinkContext, pathInfix: String? = nil, _ builder: ((String) -> Tag)? = nil ) { self.context = context self.pathInfix = pathInfix self.body = builder?(context.label) ?? Text(context.label) } @TagBuilder public func render( _ req: Request ) -> Tag { A { body } .href(context.url(req, pathInfix?.pathComponents ?? [])) .target(.blank, context.isBlank) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Html/SelectFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct SelectFieldTemplate: TemplateRepresentable { public var context: SelectFieldContext public init( _ context: SelectFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Select { for item in context.options { Option(item.label) .value(item.key) .selected(context.value == item.key) } } .name(context.key) if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Html/TableTemplate.swift ================================================ import Vapor import SwiftHtml public struct TableTemplate: TemplateRepresentable { var context: TableContext public init( _ context: TableContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Table { Thead { Tr { context.columns.map { column in Th(column.label) .id(column.key) .class("field") } context.actions.map { action in Th(action.label) .class("action") } } } Tbody { for row in context.rows { Tr { row.cells.map { CellTemplate($0, rowId: row.id).render(req) } context.actions.map { action in Td { LinkTemplate(action, pathInfix: row.id).render(req) } .class("action") } } .id(row.id) } } } } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Templates/Html/TextareaFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct TextareaFieldTemplate: TemplateRepresentable { public var context: TextareaFieldContext public init( _ context: TextareaFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Textarea(context.value) .placeholder(context.placeholder) .name(context.key) if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Validation/AsyncValidator.swift ================================================ import Vapor public protocol AsyncValidator { var key: String { get } var message: String { get } func validate( _ req: Request ) async throws -> ValidationErrorDetail? } public extension AsyncValidator { var error: ValidationErrorDetail { .init(key: key, message: message) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Validation/AsyncValidatorBuilder.swift ================================================ @resultBuilder public enum AsyncValidatorBuilder { public static func buildBlock( _ components: AsyncValidator... ) -> [AsyncValidator] { components } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Validation/FormFieldValidator+Validations.swift ================================================ import Vapor public extension FormFieldValidator where Input == String { static func required( _ field: AbstractFormField, _ message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is required" return .init(field, msg) { _, field in !field.input.isEmpty } } static func min( _ field: AbstractFormField, length: Int, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)" return .init(field, msg) { _, field in field.input.count >= length } } static func max( _ field: AbstractFormField, length: Int, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)" return .init(field, msg) { _, field in field.input.count <= length } } static func alphanumeric( _ field: AbstractFormField, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) should be only alphanumeric characters" return .init(field, msg) { _, field in !Validator .characterSet(.alphanumerics) .validate(field.input) .isFailure } } static func email( _ field: AbstractFormField, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) should be a valid email address" return .init(field, msg) { _, field in !Validator .email .validate(field.input) .isFailure } } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Validation/FormFieldValidator.swift ================================================ import Vapor public struct FormFieldValidator< Input: Decodable, Output: TemplateRepresentable >: AsyncValidator { public typealias AsyncValidationBlock = ((Request, AbstractFormField) async throws -> Bool) public let field: AbstractFormField public let message: String public let validation: AsyncValidationBlock public var key: String { field.key } public init( _ field: AbstractFormField, _ message: String, _ validation: @escaping AsyncValidationBlock ) { self.field = field self.message = message self.validation = validation } public func validate( _ req: Request ) async throws -> ValidationErrorDetail? { let isValid = try await validation(req, field) if isValid { return nil } field.error = message return error } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Validation/KeyedContentValidator+Validations.swift ================================================ import Vapor public extension KeyedContentValidator where T == String { static func required( _ key: String, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is required" return .init(key, msg, optional: optional) { value, _ in !value.isEmpty } } static func min( _ key: String, _ length: Int, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is too short (min: \(length) characters)" return .init(key, msg, optional: optional) { value, _ in value.count >= length } } static func max( _ key: String, _ length: Int, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is too long (max: \(length) characters)" return .init(key, msg, optional: optional) { value, _ in value.count <= length } } static func alphanumeric( _ key: String, _ message: String? = nil, _ optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) should be only alphanumeric characters" return .init(key, msg, optional: optional) { value, _ in !Validator.characterSet(.alphanumerics).validate(value).isFailure } } static func email( _ key: String, _ message: String? = nil, _ optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) should be a valid email address" return .init(key, msg, optional: optional) { value, _ in !Validator.email.validate(value).isFailure } } } public extension KeyedContentValidator where T == Int { static func min( _ key: String, _ length: Int, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is too short (min: \(length) characters)" return .init(key, msg, optional: optional) { value, _ in value >= length } } static func max( _ key: String, _ length: Int, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is too long (max: \(length) characters)" return .init(key, msg, optional: optional) { value, _ in value <= length } } static func contains( _ key: String, _ values: [Int], _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is an invalid value" return .init(key, msg, optional: optional) { value, _ in values.contains(value) } } } public extension KeyedContentValidator where T == UUID { static func required( _ key: String, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is required" return .init(key, msg, optional: optional) { _, _ in true } } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Validation/KeyedContentValidator.swift ================================================ import Vapor public struct KeyedContentValidator: AsyncValidator { public let key: String public let message: String public let optional: Bool public let validation: (T, Request) async throws -> Bool public init( _ key: String, _ message: String, optional: Bool = false, _ validation: @escaping (T, Request) async throws -> Bool ) { self.key = key self.message = message self.optional = optional self.validation = validation } public func validate( _ req: Request ) async throws -> ValidationErrorDetail? { let optionalValue = try? req.content.get(T.self, at: key) if let value = optionalValue { return try await validation(value, req) ? nil : error } return optional ? nil : error } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Validation/RequestValidator.swift ================================================ import Vapor public struct RequestValidator { public var validators: [AsyncValidator] public init( _ validators: [AsyncValidator] ) { self.validators = validators } public func validate( _ req: Request, message: String? = nil ) async throws { var result: [ValidationErrorDetail] = [] for validator in validators { if result.contains(where: { $0.key == validator.key }) { continue } if let res = try await validator.validate(req) { result.append(res) } } if !result.isEmpty { throw ValidationAbort( abort: Abort(.badRequest, reason: message), details: result ) } } public func isValid( _ req: Request ) async -> Bool { do { try await validate(req, message: nil) return true } catch { return false } } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Validation/ValidationAbort.swift ================================================ import Vapor public struct ValidationAbort: AbortError { public var abort: Abort public var message: String? public var details: [ValidationErrorDetail] public var reason: String { abort.reason } public var status: HTTPStatus { abort.status } public init( abort: Abort, message: String? = nil, details: [ValidationErrorDetail] ) { self.abort = abort self.message = message self.details = details } } ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Validation/ValidationError.swift ================================================ import Vapor struct ValidationError: Codable { let message: String? let details: [ValidationErrorDetail] init( message: String?, details: [ValidationErrorDetail] ) { self.message = message self.details = details } } extension ValidationError: Content {} ================================================ FILE: Chapter 13/myProject/Sources/App/Framework/Validation/ValidationErrorDetail.swift ================================================ import Vapor public struct ValidationErrorDetail: Codable { public var key: String public var message: String public init( key: String, message: String ) { self.key = key self.message = message } } extension ValidationErrorDetail: Content {} ================================================ FILE: Chapter 13/myProject/Sources/App/Middlewares/ExtendPathMiddleware.swift ================================================ import Vapor struct ExtendPathMiddleware: AsyncMiddleware { func respond( to req: Request, chainingTo next: AsyncResponder ) async throws -> Response { if !req.url.path.hasSuffix("/") && !req.url.path.contains(".") { return req.redirect( to: req.url.path + "/", redirectType: .permanent ) } return try await next.respond( to: req ) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Admin/AdminModule.swift ================================================ import Vapor struct AdminModule: ModuleInterface { let router = AdminRouter() func boot(_ app: Application) throws { try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Admin/AdminRouter.swift ================================================ import Vapor struct AdminRouter: RouteCollection { let controller = AdminFrontendController() func boot(routes: RoutesBuilder) throws { routes .grouped( AuthenticatedUser.redirectMiddleware( path: "/sign-in/" ) ) .get("admin", use: controller.dashboardView) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Admin/Controllers/AdminController.swift ================================================ import Vapor protocol AdminController: AdminListController, AdminDetailController, AdminCreateController, AdminUpdateController, AdminDeleteController { func setupRoutes( _ routes: RoutesBuilder ) } extension AdminController { func setupRoutes( _ routes: RoutesBuilder ) { setupListRoutes(routes) setupDetailRoutes(routes) setupCreateRoutes(routes) setupUpdateRoutes(routes) setupDeleteRoutes(routes) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Admin/Controllers/AdminCreateController.swift ================================================ import Vapor protocol AdminCreateController: CreateController { associatedtype CreateModelEditor: ModelEditorInterface func createTemplate( _ req: Request, _ editor: CreateModelEditor ) -> TemplateRepresentable func createView( _ req: Request ) async throws -> Response func createAction( _ req: Request ) async throws -> Response func createContext( _ req: Request, _ editor: CreateModelEditor ) -> AdminEditorPageContext func createBreadcrumbs( _ req: Request ) -> [LinkContext] func setupCreateRoutes( _ routes: RoutesBuilder ) } extension AdminCreateController { private func render( _ req: Request, editor: CreateModelEditor ) -> Response { req.templates.renderHtml( createTemplate(req, editor) ) } func createView( _ req: Request ) async throws -> Response { let editor = CreateModelEditor( model: .init(), form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) return render(req, editor: editor) } func createAction( _ req: Request ) async throws -> Response { let model = DatabaseModel() let editor = CreateModelEditor( model: model as! CreateModelEditor.Model, form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) try await editor.process(req: req) let isValid = try await editor.validate(req: req) guard isValid else { return render(req, editor: editor) } try await editor.write(req: req) try await create(req, editor.model as! DatabaseModel) try await editor.save(req: req) var components = req.url.path.pathComponents.dropLast() components += editor.model.id!.uuidString.pathComponents return req.redirect(to: "/" + components.string + "/update/") } func createTemplate( _ req: Request, _ editor: CreateModelEditor ) -> TemplateRepresentable { AdminEditorPageTemplate( createContext(req, editor) ) } func createContext( _ req: Request, _ editor: CreateModelEditor ) -> AdminEditorPageContext { let context = FormContext( action: editor.form.action, fields: editor.form.fields.map { $0.render(req: req) }, error: editor.form.error, submit: editor.form.submit ) return .init( title: "Create", form: context, breadcrumbs: createBreadcrumbs(req) ) } func createBreadcrumbs( _ req: Request ) -> [LinkContext] { [ LinkContext( label: Self.moduleName.capitalized, dropLast: 2 ), LinkContext( label: Self.modelName.plural.capitalized, dropLast: 1 ), ] } func setupCreateRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) baseRoutes.get("create", use: createView) baseRoutes.post("create", use: createAction) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Admin/Controllers/AdminDeleteController.swift ================================================ import Vapor final class DeleteForm: AbstractForm { init() { super.init() self.action.method = .post self.submit = "Delete" } } protocol AdminDeleteController: DeleteController { func deleteView( _ req: Request ) async throws -> Response func deleteAction( _ req: Request ) async throws -> Response func deleteTemplate( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> TemplateRepresentable func deleteInfo( _ model: DatabaseModel ) -> String func deleteContext( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> AdminDeletePageContext func setupDeleteRoutes( _ routes: RoutesBuilder ) } extension AdminDeleteController { func deleteView( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let form = DeleteForm() return req.templates.renderHtml( deleteTemplate(req, model, form) ) } func deleteAction( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) try await delete(req, model) var url = req.url.path if let redirect = try? req.query.get(String.self, at: "redirect") { url = redirect } return req.redirect(to: url) } func deleteTemplate( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> TemplateRepresentable { AdminDeletePageTemplate( deleteContext(req, model, form) ) } func deleteContext( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> AdminDeletePageContext { .init( title: "Delete", name: deleteInfo(model), type: "model", form: form.getContext(req) ) } func setupDeleteRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.get("delete", use: deleteView) existingModelRoutes.post("delete", use: deleteAction) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Admin/Controllers/AdminDetailController.swift ================================================ import Vapor protocol AdminDetailController: DetailController { func detailView( _ req: Request ) async throws -> Response func detailTemplate( _ req: Request, _ model: DatabaseModel ) -> TemplateRepresentable func detailFields( for model: DatabaseModel ) -> [DetailContext] func detailContext( _ req: Request, _ model: DatabaseModel ) -> AdminDetailPageContext func detailBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func detailNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func setupDetailRoutes( _ routes: RoutesBuilder ) } extension AdminDetailController { func detailView( _ req: Request ) async throws -> Response { let model = try await detail(req) return req.templates.renderHtml(detailTemplate(req, model)) } func detailTemplate( _ req: Request, _ model: DatabaseModel ) -> TemplateRepresentable { AdminDetailPageTemplate(detailContext(req, model)) } func detailContext( _ req: Request, _ model: DatabaseModel ) -> AdminDetailPageContext { let path = "/delete/?redirect=" + req.url.path.pathComponents.dropLast().string + "&cancel=" + req.url.path return .init( title: "Details", fields: detailFields(for: model), navigation: detailNavigation(req, model), breadcrumbs: detailBreadcrumbs(req, model), actions: [ LinkContext( label: "Delete", path: path ), ] ) } func detailBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: Self.moduleName.capitalized, dropLast: 2 ), LinkContext( label: Self.modelName.plural.capitalized, dropLast: 1 ), ] } func detailNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: "Update", path: "update" ), ] } func setupDetailRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.get(use: detailView) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Admin/Controllers/AdminFrontendController.swift ================================================ import Vapor struct AdminFrontendController { func dashboardView( req: Request ) throws -> Response { let user = try req.auth.require(AuthenticatedUser.self) let template = AdminDashboardTemplate( .init( icon: "👋", title: "Dashboard", message: "Hello \(user.email), welcome to the CMS." ) ) return req.templates.renderHtml(template) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Admin/Controllers/AdminListController.swift ================================================ import Vapor protocol AdminListController: ListController { func listView( _ req: Request ) async throws -> Response func listColumns() -> [ColumnContext] func listCells( for model: DatabaseModel ) -> [CellContext] func listNavigation( _ req: Request ) -> [LinkContext] func listBreadcrumbs( _ req: Request ) -> [LinkContext] func listContext( _ req: Request, _ list: [DatabaseModel] ) -> AdminListPageContext func listTemplate( _ req: Request, _ list: [DatabaseModel] ) -> TemplateRepresentable func setupListRoutes( _ routes: RoutesBuilder ) } extension AdminListController { func listView(_ req: Request) async throws -> Response { let list = try await list(req) let template = listTemplate(req, list) return req.templates.renderHtml(template) } func listNavigation(_ req: Request) -> [LinkContext] { [ LinkContext(label: "Create",path: "create") ] } func listBreadcrumbs(_ req: Request) -> [LinkContext] { [ LinkContext( label: DatabaseModel.Module.identifier.capitalized, dropLast: 1 ) ] } func listContext( _ req: Request, _ list: [DatabaseModel] ) -> AdminListPageContext { let rows = list.map { RowContext(id: $0.id!.uuidString, cells: listCells(for: $0)) } let table = TableContext(columns: listColumns(), rows: rows, actions: [ LinkContext(label: "Update", path: "update"), LinkContext(label: "Delete", path: "delete") ]) return .init( title: "List", table: table, navigation: listNavigation(req), breadcrumbs: listBreadcrumbs(req) ) } func listTemplate( _ req: Request, _ list: [DatabaseModel] ) -> TemplateRepresentable { AdminListPageTemplate(listContext(req, list)) } func setupListRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) baseRoutes.get(use: listView) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Admin/Controllers/AdminUpdateController.swift ================================================ import Vapor protocol AdminUpdateController: UpdateController { associatedtype UpdateModelEditor: ModelEditorInterface func updateView( _ req: Request ) async throws -> Response func updateAction( _ req: Request ) async throws -> Response func updateTemplate( _ req: Request, _ editor: UpdateModelEditor ) async -> TemplateRepresentable func updateContext( _ req: Request, _ editor: UpdateModelEditor ) async -> AdminEditorPageContext func updateBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func updateNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func setupUpdateRoutes( _ routes: RoutesBuilder ) } extension AdminUpdateController { private func render( _ req: Request, editor: UpdateModelEditor ) async -> Response { req.templates.renderHtml( await updateTemplate(req, editor) ) } func updateView( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let editor = UpdateModelEditor( model: model as! UpdateModelEditor.Model, form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) try await editor.read(req: req) return await render(req, editor: editor) } func updateAction( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let editor = UpdateModelEditor( model: model as! UpdateModelEditor.Model, form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) try await editor.process(req: req) let isValid = try await editor.validate(req: req) guard isValid else { return await render(req, editor: editor) } try await editor.write(req: req) try await update(req, editor.model as! DatabaseModel) try await editor.save(req: req) return req.redirect(to: req.url.path) } func updateTemplate( _ req: Request, _ editor: UpdateModelEditor ) async -> TemplateRepresentable { await AdminEditorPageTemplate( updateContext(req, editor) ) } func updateContext( _ req: Request, _ editor: UpdateModelEditor ) async -> AdminEditorPageContext { let path = "delete/?redirect=" + req.url.path.pathComponents.dropLast(2).string + "&cancel=" + req.url.path let model = editor.model as! DatabaseModel return .init( title: "Update", form: editor.form.getContext(req), navigation: updateNavigation(req, model), breadcrumbs: updateBreadcrumbs(req, model), actions: [ LinkContext( label: "Delete", path: path, dropLast: 1 ), ] ) } func updateBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: Self.moduleName.capitalized, dropLast: 3 ), LinkContext( label: Self.modelName.plural.capitalized, dropLast: 2 ), ] } func updateNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: "Details", dropLast: 1 ), ] } func setupUpdateRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.get("update", use: updateView) existingModelRoutes.post("update", use: updateAction) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDashboardContext.swift ================================================ struct AdminDashboardContext { let icon: String let title: String let message: String } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDeletePageContext.swift ================================================ public struct AdminDeletePageContext { public let title: String public let name: String public let type: String public let form: FormContext public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public init( title: String, name: String, type: String, form: FormContext, navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [] ) { self.title = title self.name = name self.type = type self.form = form self.navigation = navigation self.breadcrumbs = breadcrumbs } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDetailPageContext.swift ================================================ public struct AdminDetailPageContext { public let title: String public let fields: [DetailContext] public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public let actions: [LinkContext] public init( title: String, fields: [DetailContext], navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [], actions: [LinkContext] = [] ) { self.title = title self.fields = fields self.navigation = navigation self.breadcrumbs = breadcrumbs self.actions = actions } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminEditorPageContext.swift ================================================ public struct AdminEditorPageContext { public let title: String public let form: FormContext public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public let actions: [LinkContext] public init( title: String, form: FormContext, navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [], actions: [LinkContext] = [] ) { self.title = title self.form = form self.navigation = navigation self.breadcrumbs = breadcrumbs self.actions = actions } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminIndexContext.swift ================================================ public struct AdminIndexContext { public let title: String public let breadcrumbs: [LinkContext] public init( title: String, breadcrumbs: [LinkContext] = [] ) { self.title = title self.breadcrumbs = breadcrumbs } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminListPageContext.swift ================================================ public struct AdminListPageContext { public let title: String public let table: TableContext public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public init( title: String, table: TableContext, navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [] ) { self.title = title self.table = table self.navigation = navigation self.breadcrumbs = breadcrumbs } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDashboardTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminDashboardTemplate: TemplateRepresentable { var context: AdminDashboardContext init( _ context: AdminDashboardContext ) { self.context = context } @TagBuilder func render(_ req: Request) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } Nav { H2("Blog") Ul { Li { A("Posts") .href("/admin/blog/posts/") } Li { A("Categories") .href("/admin/blog/categories/") } } } } .id("dashboard") .class("container") } .render(req) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDeletePageTemplate.swift ================================================ import Vapor import SwiftHtml public struct AdminDeletePageTemplate: TemplateRepresentable { var context: AdminDeletePageContext public init( _ context: AdminDeletePageContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { Span("🗑") .class("icon") H1(context.title) P("You are about to permanently delete the
`\(context.name)` \(context.type).") FormTemplate(context.form).render(req) A("Cancel") .href((try? req.query.get(String.self, at: "cancel")) ?? "#") .class(["button", "cancel"]) } .class(["lead", "container", "center"]) } .render(req) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDetailPageTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminDetailPageTemplate: TemplateRepresentable { var context: AdminDetailPageContext init( _ context: AdminDetailPageContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { Div { H1(context.title) for item in context.navigation { LinkTemplate(item).render(req) } } .class("lead") Dl { for item in context.fields { DetailTemplate(item).render(req) } } Section { for item in context.actions { LinkTemplate(item).render(req) } } } .class("container") } .render(req) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Admin/Templates/Html/AdminEditorPageTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminEditorPageTemplate: TemplateRepresentable { var context: AdminEditorPageContext init( _ context: AdminEditorPageContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { Div { H1(context.title) for item in context.navigation { LinkTemplate(item).render(req) } } .class("lead") FormTemplate(context.form).render(req) Section { for item in context.actions { LinkTemplate(item).render(req) } } } .class("container") } .render(req) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Admin/Templates/Html/AdminIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg public struct AdminIndexTemplate: TemplateRepresentable { public var context: AdminIndexContext var body: Tag public init( _ context: AdminIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Meta() .name("robots") .content("noindex") Link(rel: .shortcutIcon) .href("/images/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css") Link(rel: .stylesheet) .href("/css/admin.css") Title(context.title) } Body { Div { A { Img(src: "/img/logo.png", alt: "Logo") .title("Logo") .style("width: 300px") } .href("/") Nav { Input() .type(.checkbox) .id("secondary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("secondary-menu-button") Div { A("Sign out") .href("/sign-out/") } .class("menu-items") } .id("secondary-menu") } .id("navigation") Div { Nav { A("Admin") .href("/admin/") for breadcrumb in context.breadcrumbs { LinkTemplate(breadcrumb).render(req) } } } .class("breadcrumb") Main { body } Script() .type(.javascript) .src("/js/admin.js") } } .lang("en-US") } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Admin/Templates/Html/AdminListPageTemplate.swift ================================================ import Vapor import SwiftHtml public struct AdminListPageTemplate: TemplateRepresentable { var context: AdminListPageContext public init( _ context: AdminListPageContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { H1(context.title) P { context.navigation.map { LinkTemplate($0).render(req) } } } .class("lead") if context.table.rows.isEmpty { Div { Span("🔍") .class("icon") H2("Oh no") P("This list is empty right now.") A("Try again →") .href(req.url.path) .class("button-1") } .class(["lead", "container", "center"]) } else { TableTemplate(context.table).render(req) } } .render(req) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Api/ApiModule.swift ================================================ import Vapor struct ApiModule: ModuleInterface { func boot(_ app: Application) throws { app.middleware.use(ApiErrorMiddleware()) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Api/Controllers/ApiController.swift ================================================ import Vapor protocol ApiController: ApiListController, ApiDetailController, ApiCreateController, ApiUpdateController, ApiPatchController, ApiDeleteController { func validators( optional: Bool ) -> [AsyncValidator] func setupRoutes( _ routes: RoutesBuilder ) } extension ApiController { func validators( optional: Bool ) -> [AsyncValidator] { [] } func createValidators() -> [AsyncValidator] { validators(optional: false) } func updateValidators() -> [AsyncValidator] { validators(optional: false) } func patchValidators() -> [AsyncValidator] { validators(optional: true) } func createResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response { try await detailOutput(req, model) .encodeResponse(status: .created, for: req) } func updateResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response { try await detailOutput(req, model) .encodeResponse(for: req) } func patchResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response { try await detailOutput(req, model) .encodeResponse(for: req) } func setupRoutes( _ routes: RoutesBuilder ) { setupListRoutes(routes) setupDetailRoutes(routes) setupCreateRoutes(routes) setupUpdateRoutes(routes) setupPatchRoutes(routes) setupDeleteRoutes(routes) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Api/Controllers/ApiCreateController.swift ================================================ import Vapor protocol ApiCreateController: CreateController { associatedtype CreateObject: Decodable func createValidators() -> [AsyncValidator] func createInput( _ req: Request, _ model: DatabaseModel, _ input: CreateObject ) async throws func createApi( _ req: Request ) async throws -> Response func createResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response func setupCreateRoutes( _ routes: RoutesBuilder ) } extension ApiCreateController { func createValidators() -> [AsyncValidator] { [] } func createApi( _ req: Request ) async throws -> Response { try await RequestValidator(createValidators()).validate(req) let input = try req.content.decode(CreateObject.self) let model = DatabaseModel() try await createInput(req, model, input) try await create(req, model) return try await createResponse(req, model) } func setupCreateRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) baseRoutes.post(use: createApi) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Api/Controllers/ApiDeleteController.swift ================================================ import Vapor public protocol ApiDeleteController: DeleteController { func deleteApi( _ req: Request ) async throws -> HTTPStatus func setupDeleteRoutes( _ routes: RoutesBuilder ) } public extension ApiDeleteController { func deleteApi( _ req: Request ) async throws -> HTTPStatus { let model = try await findBy(identifier(req), on: req.db) try await delete(req, model) return .noContent } func setupDeleteRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.delete(use: deleteApi) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Api/Controllers/ApiDetailController.swift ================================================ import Vapor protocol ApiDetailController: DetailController { associatedtype DetailObject: Content func detailOutput( _ req: Request, _ model: DatabaseModel ) async throws -> DetailObject func detailApi( _ req: Request ) async throws -> DetailObject func setupDetailRoutes( _ routes: RoutesBuilder ) } extension ApiDetailController { func detailApi( _ req: Request ) async throws -> DetailObject { let model = try await detail(req) return try await detailOutput(req, model) } func setupDetailRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.get(use: detailApi) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Api/Controllers/ApiListController.swift ================================================ import Vapor protocol ApiListController: ListController { associatedtype ListObject: Content func listOutput( _ req: Request, _ models: [DatabaseModel] ) async throws -> [ListObject] func listApi( _ req: Request ) async throws -> [ListObject] func setupListRoutes( _ routes: RoutesBuilder ) } extension ApiListController { func listApi( _ req: Request ) async throws -> [ListObject] { let models = try await list(req) return try await listOutput(req, models) } func setupListRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) baseRoutes.get(use: listApi) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Api/Controllers/ApiPatchController.swift ================================================ import Vapor public protocol ApiPatchController: PatchController { associatedtype PatchObject: Decodable func patchValidators() -> [AsyncValidator] func patchInput( _ req: Request, _ model: DatabaseModel, _ input: PatchObject ) async throws func patchApi( _ req: Request ) async throws -> Response func patchResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response func setupPatchRoutes( _ routes: RoutesBuilder ) } public extension ApiPatchController { func patchValidators() -> [AsyncValidator] { [] } func patchApi( _ req: Request ) async throws -> Response { try await RequestValidator(patchValidators()).validate(req) let model = try await findBy(identifier(req), on: req.db) let input = try req.content.decode(PatchObject.self) try await patchInput(req, model, input) try await patch(req, model) return try await patchResponse(req, model) } func setupPatchRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.patch(use: patchApi) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Api/Controllers/ApiUpdateController.swift ================================================ import Vapor public protocol ApiUpdateController: UpdateController { associatedtype UpdateObject: Decodable func updateValidators() -> [AsyncValidator] func updateInput( _ req: Request, _ model: DatabaseModel, _ input: UpdateObject ) async throws func updateApi( _ req: Request ) async throws -> Response func updateResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response func setupUpdateRoutes( _ routes: RoutesBuilder ) } public extension ApiUpdateController { func updateValidators() -> [AsyncValidator] { [] } func updateApi( _ req: Request ) async throws -> Response { try await RequestValidator(updateValidators()).validate(req) let model = try await findBy(identifier(req), on: req.db) let input = try req.content.decode(UpdateObject.self) try await updateInput(req, model, input) try await update(req, model) return try await updateResponse(req, model) } func setupUpdateRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.put(use: updateApi) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Api/Middlewares/ApiErrorMiddleware.swift ================================================ import Vapor struct ApiErrorMiddleware: AsyncMiddleware { func respond( to req: Request, chainingTo next: AsyncResponder ) async throws -> Response { do { return try await next.respond(to: req) } catch { let status: HTTPResponseStatus let headers: HTTPHeaders let message: String? let details: [ValidationErrorDetail] switch error { case let abort as ValidationAbort: status = abort.abort.status headers = abort.abort.headers message = abort.message details = abort.details case let abort as Abort: status = abort.status headers = abort.headers message = abort.reason details = [] default: status = .internalServerError headers = [:] if req.application.environment.isRelease { message = "Something went wrong." } else { message = error.localizedDescription } details = [] } req.logger.report(error: error) let response = Response( status: status, headers: headers ) do { let encoder = JSONEncoder() let data = try encoder.encode( ValidationError( message: message, details: details ) ) response.body = .init(data: data) response.headers.replaceOrAdd( name: .contentType, value: "application/json; charset=utf-8" ) } catch { response.body = .init(string: "Oops: \(error)") response.headers.replaceOrAdd( name: .contentType, value: "text/plain; charset=utf-8" ) } return response } } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/BlogModule.swift ================================================ import Vapor struct BlogModule: ModuleInterface { let router = BlogRouter() func boot(_ app: Application) throws { app.migrations.add(BlogMigrations.v1()) app.migrations.add(BlogMigrations.seed()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/BlogRouter.swift ================================================ import Vapor struct BlogRouter: RouteCollection { let frontendController = BlogFrontendController() let postAdminController = BlogPostAdminController() let postApiController = BlogPostApiController() let categoryAdminController = BlogCategoryAdminController() let categoryApiController = BlogCategoryApiController() func boot(routes: RoutesBuilder) throws { routes.get("blog", use: frontendController.blogView) routes.get(.anything, use: frontendController.postView) let admin = routes .grouped(AuthenticatedUser.redirectMiddleware(path: "/")) .grouped("admin") postAdminController.setupRoutes(admin) categoryAdminController.setupRoutes(admin) let api = routes .grouped(AuthenticatedUser.guardMiddleware()) .grouped("api") postApiController.setupRoutes(api) categoryApiController.setupRoutes(api) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/Controllers/BlogCategoryAdminController.swift ================================================ import Vapor import Fluent struct BlogCategoryAdminController: AdminController { typealias ApiModel = Blog.Category typealias DatabaseModel = BlogCategoryModel typealias CreateModelEditor = BlogCategoryEditor typealias UpdateModelEditor = BlogCategoryEditor func listColumns() -> [ColumnContext] { [ .init("title"), ] } func listCells( for model: DatabaseModel ) -> [CellContext] { [ .init( model.title, link: .init(label: model.title) ), ] } func detailFields( for model: DatabaseModel ) -> [DetailContext] { [ .init("title", model.title), ] } func deleteInfo( _ model: DatabaseModel ) -> String { model.title } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/Controllers/BlogCategoryApiController.swift ================================================ import Vapor extension Blog.Category.List: Content {} extension Blog.Category.Detail: Content {} struct BlogCategoryApiController: ApiController { typealias ApiModel = Blog.Category typealias DatabaseModel = BlogCategoryModel @AsyncValidatorBuilder func validators( optional: Bool ) -> [AsyncValidator] { KeyedContentValidator.required("title", optional: optional) } func listOutput( _ req: Request, _ models: [BlogCategoryModel] ) async throws -> [Blog.Category.List] { models.map { .init(id: $0.id!, title: $0.title) } } func detailOutput( _ req: Request, _ model: BlogCategoryModel ) async throws -> Blog.Category.Detail { .init( id: model.id!, title: model.title ) } func createInput( _ req: Request, _ model: BlogCategoryModel, _ input: Blog.Category.Create ) async throws { model.title = input.title } func updateInput( _ req: Request, _ model: BlogCategoryModel, _ input: Blog.Category.Update ) async throws { model.title = input.title } func patchInput( _ req: Request, _ model: BlogCategoryModel, _ input: Blog.Category.Patch ) async throws { model.title = input.title ?? model.title } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift ================================================ import Vapor import Fluent struct BlogFrontendController { func blogView(req: Request) async throws -> Response { let posts = try await BlogPostModel .query(on: req.db) .sort(\.$date, .descending) .all() let api = BlogPostApiController() let listOutput = try await api.listOutput(req, posts) let ctx = BlogPostsContext( icon: "🔥", title: "Blog", message: "Hot news and stories about everything.", posts: listOutput ) return req.templates.renderHtml(BlogPostsTemplate(ctx)) } func postView(req: Request) async throws -> Response { let slug = req.url.path.trimmingCharacters( in: .init(charactersIn: "/") ) guard let post = try await BlogPostModel .query(on: req.db) .filter(\.$slug == slug) .with(\.$category) .first() else { return req.redirect(to: "/") } let api = BlogPostApiController() let postOutput = try await api.detailOutput(req, post) let ctx = BlogPostContext(post: postOutput) return req.templates.renderHtml(BlogPostTemplate(ctx)) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/Controllers/BlogPostAdminController.swift ================================================ import Vapor import Fluent struct BlogPostAdminController: AdminController { typealias ApiModel = Blog.Post typealias DatabaseModel = BlogPostModel typealias CreateModelEditor = BlogPostEditor typealias UpdateModelEditor = BlogPostEditor func listColumns() -> [ColumnContext] { [ .init("image"), .init("title"), ] } func listCells(for model: DatabaseModel) -> [CellContext] { [ .init(model.imageKey, type: .image), .init(model.title, link: .init(label: model.title)), ] } func detailFields(for model: DatabaseModel) -> [DetailContext] { [ .init("image", model.imageKey, type: .image), .init("title", model.title), ] } func deleteInfo( _ model: DatabaseModel ) -> String { model.title } func beforeDelete( _ req: Request, _ model: BlogPostModel ) async throws { try await req.fs.delete(key: model.imageKey) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/Controllers/BlogPostApiController.swift ================================================ import Vapor extension Blog.Post.List: Content {} extension Blog.Post.Detail: Content {} struct BlogPostApiController: ApiController { typealias ApiModel = Blog.Post typealias DatabaseModel = BlogPostModel @AsyncValidatorBuilder func validators(optional: Bool) -> [AsyncValidator] { KeyedContentValidator.required("title", optional: optional) KeyedContentValidator.required("slug", optional: optional) KeyedContentValidator.required("image", optional: optional) KeyedContentValidator.required("excerpt", optional: optional) KeyedContentValidator.required("content", optional: optional) KeyedContentValidator.required("categoryId", optional: optional) KeyedContentValidator("categoryId", "Invalid or missing category", optional: optional) { value, req in try await BlogCategoryModel.find(value, on: req.db) != nil } } func listOutput( _ req: Request, _ models: [DatabaseModel] ) async throws -> [Blog.Post.List] { models.map { model in .init( id: model.id!, title: model.title, slug: model.slug, image: model.imageKey, excerpt: model.excerpt, date: model.date ) } } func detailOutput( _ req: Request, _ model: DatabaseModel ) async throws -> Blog.Post.Detail { .init( id: model.id!, title: model.title, slug: model.slug, image: model.imageKey, excerpt: model.excerpt, date: model.date, category: .init( id: model.category.id!, title: model.category.title ), content: model.content ) } func createInput( _ req: Request, _ model: DatabaseModel, _ input: Blog.Post.Create ) async throws { model.title = input.title model.slug = input.slug model.imageKey = input.image model.excerpt = input.excerpt model.date = input.date model.content = input.content } func updateInput( _ req: Request, _ model: DatabaseModel, _ input: Blog.Post.Update ) async throws { model.title = input.title model.slug = input.slug model.imageKey = input.image model.excerpt = input.excerpt model.date = input.date model.content = input.content } func patchInput( _ req: Request, _ model: DatabaseModel, _ input: Blog.Post.Patch ) async throws { model.title = input.title ?? model.title model.slug = input.slug ?? model.slug model.imageKey = input.image ?? model.imageKey model.excerpt = input.excerpt ?? model.excerpt model.date = input.date ?? model.date model.content = input.content ?? model.content } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/Database/Migrations/BlogMigrations.swift ================================================ import Foundation import Fluent enum BlogMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema) .id() .field(BlogCategoryModel.FieldKeys.v1.title, .string, .required) .create() try await db.schema(BlogPostModel.schema) .id() .field(BlogPostModel.FieldKeys.v1.title, .string, .required) .field(BlogPostModel.FieldKeys.v1.slug, .string, .required) .field(BlogPostModel.FieldKeys.v1.imageKey, .string, .required) .field(BlogPostModel.FieldKeys.v1.excerpt, .data, .required) .field(BlogPostModel.FieldKeys.v1.date, .datetime, .required) .field(BlogPostModel.FieldKeys.v1.content, .data, .required) .field(BlogPostModel.FieldKeys.v1.categoryId, .uuid) .foreignKey( BlogPostModel.FieldKeys.v1.categoryId, references: BlogCategoryModel.schema, .id, onDelete: DatabaseSchema.ForeignKeyAction.setNull, onUpdate: .cascade ) .unique(on: BlogPostModel.FieldKeys.v1.slug) .create() } func revert(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema).delete() try await db.schema(BlogPostModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let categories = (1...4).map { index in BlogCategoryModel(title: "Sample category #\(index)") } try await categories.create(on: db) try await (1...9).map { index in BlogPostModel( id: nil, title: "Sample post #\(index)", slug: "sample-post-\(index)", imageKey: "/img/posts/\(String(format: "%02d", index + 1)).jpg", excerpt: "Lorem ipsum", date: Date().addingTimeInterval(-Double.random(in: 0...(86400 * 60))), content: "Lorem ipsum dolor sit amet.", categoryId: categories[Int.random(in: 0.. [FormComponent] { ImageField("image", path: "blog/post") .read { [unowned self] in $1.output.context.previewUrl = model.imageKey ($1 as! ImageField).imageKey = model.imageKey } .write { [unowned self] in model.imageKey = ($1 as! ImageField).imageKey ?? "" } InputField("slug") .config { $0.output.context.label.required = true } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = model.slug } .write { [unowned self] in model.slug = $1.input } InputField("title") .config { $0.output.context.label.required = true } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = model.title } .write { [unowned self] in model.title = $1.input } InputField("date") .config { $0.output.context.label.required = true $0.output.context.value = dateFormatter.string(from: Date()) } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = dateFormatter.string(from: model.date) } .write { [unowned self] in model.date = dateFormatter.date(from: $1.input) ?? Date() } TextareaField("excerpt") .read { [unowned self] in $1.output.context.value = model.excerpt } .write { [unowned self] in model.excerpt = $1.input } TextareaField("content") .read { [unowned self] in $1.output.context.value = model.content } .write { [unowned self] in model.content = $1.input } SelectField("category") .load { req, field in let categories = try await BlogCategoryModel .query(on: req.db) .all() field.output.context.options = categories.map { OptionContext(key: $0.id!.uuidString, label: $0.title) } } .read { [unowned self] req, field in field.output.context.value = model.$category.id.uuidString } .write { [unowned self] req, field in if let uuid = UUID(uuidString: field.input), let category = try await BlogCategoryModel .find(uuid, on: req.db) { model.$category.id = category.id! } } } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/Objects/Blog.swift ================================================ enum Blog: ApiModuleInterface { enum Post: ApiModelInterface { typealias Module = Blog } enum Category: ApiModelInterface { typealias Module = Blog static let pathKey: String = "categories" } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/Objects/BlogCategory.swift ================================================ import Foundation extension Blog.Category { struct List: Codable { let id: UUID let title: String } struct Detail: Codable { let id: UUID let title: String } struct Create: Codable { let title: String } struct Update: Codable { let title: String } struct Patch: Codable { let title: String? } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/Objects/BlogPost.swift ================================================ import Foundation extension Blog.Post { struct List: Codable { let id: UUID let title: String let slug: String let image: String let excerpt: String let date: Date } struct Detail: Codable { let id: UUID let title: String let slug: String let image: String let excerpt: String let date: Date let category: Blog.Category.List let content: String } struct Create: Codable { let title: String let slug: String let image: String let excerpt: String let date: Date let content: String } struct Update: Codable { let title: String let slug: String let image: String let excerpt: String let date: Date let content: String } struct Patch: Codable { let title: String? let slug: String? let image: String? let excerpt: String? let date: Date? let content: String? } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminDeleteContext.swift ================================================ struct BlogPostAdminDeleteContext { let title: String let name: String let type: String } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminDetailContext.swift ================================================ struct BlogPostAdminDetailContext { let title: String let detail: Blog.Post.Detail } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminEditContext.swift ================================================ struct BlogPostAdminEditContext { let title: String let form: TemplateRepresentable } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminListContext.swift ================================================ struct BlogPostAdminListContext { let title: String let list: [Blog.Post.List] } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostContext.swift ================================================ struct BlogPostContext { let post: Blog.Post.Detail } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostsContext.swift ================================================ struct BlogPostsContext { let icon: String let title: String let message: String let posts: [Blog.Post.List] } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminDeleteTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminDeleteTemplate: TemplateRepresentable { var context: BlogPostAdminDeleteContext init( _ context: BlogPostAdminDeleteContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Span("🗑") .class("icon") H1(context.title) P("You are about to permanently delete the
`\(context.name)` \(context.type).") Form { Input() .type(.submit) .class(["button", "destructive"]) .style("display: inline") .value("Delete") A("Cancel") .href("/admin/blog/posts/") .class(["button", "cancel"]) } .method(.post) .id("delete-form") } .class(["lead", "container", "center"]) } .render(req) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminDetailTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminDetailTemplate: TemplateRepresentable { var context: BlogPostAdminDetailContext init( _ context: BlogPostAdminDetailContext ) { self.context = context } var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .short return formatter }() @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") Dl { Dt("Image") Dd { Img( src: context.detail.image, alt: context.detail.title ) } Dt("Title") Dd(context.detail.title) Dt("Excerpt") Dd(context.detail.excerpt) Dt("Date") Dd(dateFormatter.string(from: context.detail.date)) Dt("Content") Dd(context.detail.content) } } .id("detail") .class("container") } .render(req) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminEditTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminEditTemplate: TemplateRepresentable { var context: BlogPostAdminEditContext init( _ context: BlogPostAdminEditContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") context.form.render(req) } .id("edit") .class("container") } .render(req) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminListTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminListTemplate: TemplateRepresentable { var context: BlogPostAdminListContext init( _ context: BlogPostAdminListContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") Table { Thead { Tr { Th("Image") Th("Title") Th("Preview") } } Tbody { for item in context.list { Tr { Td { Img(src: item.image, alt: item.title) } Td { A(item.title) .href("/admin/blog/posts/" + item.id.uuidString + "/") } Td { A("Preview") .href("/" + item.slug + "/") } } } } } } .id("list") } .render(req) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostTemplate: TemplateRepresentable { var context: BlogPostContext init( _ context: BlogPostContext ) { self.context = context } var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .short return formatter }() @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.post.title) ) { Div { Section { P(dateFormatter.string(from: context.post.date)) H1(context.post.title) P(context.post.excerpt) } .class(["lead", "container"]) Img(src: context.post.image, alt: context.post.title) Article { Text(context.post.content) } .class("container") } .id("post") } .render(req) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostsTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostsTemplate: TemplateRepresentable { var context: BlogPostsContext init( _ context: BlogPostsContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") Div { for post in context.posts { Article { A { Img(src: post.image, alt: post.title) H2(post.title) P(post.excerpt) } .href("/\(post.slug)/") } } } .class("grid-221") } .id("blog") } .render(req) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/User/Authenticators/UserCredentialsAuthenticator.swift ================================================ import Vapor import Fluent struct UserCredentialsAuthenticator: AsyncCredentialsAuthenticator { struct Credentials: Content { let email: String let password: String } func authenticate( credentials: Credentials, for req: Request ) async throws { guard let user = try await UserAccountModel .query(on: req.db) .filter(\.$email == credentials.email) .first() else { return } do { guard try Bcrypt.verify( credentials.password, created: user.password ) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } catch { // do nothing... } } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/User/Authenticators/UserSessionAuthenticator.swift ================================================ import Vapor import Fluent struct UserSessionAuthenticator: AsyncSessionAuthenticator { typealias User = AuthenticatedUser func authenticate( sessionID: User.SessionID, for req: Request ) async throws { guard let user = try await UserAccountModel .find(sessionID, on: req.db) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/User/Authenticators/UserTokenAuthenticator.swift ================================================ import Vapor import Fluent struct UserTokenAuthenticator: AsyncBearerAuthenticator { func authenticate( bearer: BearerAuthorization, for req: Request ) async throws { guard let token = try await UserTokenModel .query(on: req.db) .filter(\.$value == bearer.token) .first() else { return } guard let user = try await UserAccountModel .find(token.$user.id, on: req.db) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/User/Controllers/UserApiController.swift ================================================ import Vapor extension User.Token.Detail: Content {} struct UserApiController { func signInApi( req: Request ) async throws -> User.Token.Detail { guard let user = req.auth.get(AuthenticatedUser.self) else { throw Abort(.unauthorized) } let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789=" let tokenValue = String((0..<64).map { _ in letters.randomElement()! }) let token = UserTokenModel( value: tokenValue, userId: user.id ) try await token.create(on: req.db) let account = User.Account.Detail( id: user.id, email: user.email ) return .init( id: token.id!, value: token.value, user: account ) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/User/Controllers/UserFrontendController.swift ================================================ import Vapor struct UserFrontendController { private func renderSignInView( _ req: Request, _ form: UserLoginForm ) -> Response { let template = UserLoginTemplate( .init( icon: "⬇️", title: "Sign in", message: "Please log in with your existing account", form: form.render(req: req) ) ) return req.templates.renderHtml(template) } func signInView( _ req: Request ) async throws -> Response { renderSignInView(req, .init()) } func signInAction( _ req: Request ) async throws -> Response { /// the user is authenticated, we can store the user data inside the session too if let user = req.auth.get(AuthenticatedUser.self) { req.session.authenticate(user) return req.redirect(to: "/") } let form = UserLoginForm() try await form.process(req: req) let isValid = try await form.validate(req: req) if !isValid { form.error = "Invalid email or password." } return renderSignInView(req, form) } func signOut( _ req: Request ) throws -> Response { req.auth.logout(AuthenticatedUser.self) req.session.unauthenticate(AuthenticatedUser.self) return req.redirect(to: "/") } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/User/Database/Migrations/UserMigrations.swift ================================================ import Vapor import Fluent enum UserMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(UserAccountModel.schema) .id() .field(UserAccountModel.FieldKeys.v1.email, .string, .required) .field(UserAccountModel.FieldKeys.v1.password, .string, .required) .unique(on: UserAccountModel.FieldKeys.v1.email) .create() try await db.schema(UserTokenModel.schema) .id() .field(UserTokenModel.FieldKeys.v1.value, .string, .required) .field(UserTokenModel.FieldKeys.v1.userId, .uuid, .required) .foreignKey( UserTokenModel.FieldKeys.v1.userId, references: UserAccountModel.schema, .id ) .unique(on: UserTokenModel.FieldKeys.v1.value) .create() } func revert(on db: Database) async throws { try await db.schema(UserTokenModel.schema).delete() try await db.schema(UserAccountModel.schema).delete() } } // ... struct seed: AsyncMigration { func prepare(on db: Database) async throws { let email = "root@localhost.com" let password = "ChangeMe1" let user = UserAccountModel( email: email, password: try Bcrypt.hash(password) ) try await user.create(on: db) } func revert(on db: Database) async throws { try await UserAccountModel.query(on: db).delete() } } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/User/Database/Models/UserAccountModel.swift ================================================ import Vapor import Fluent final class UserAccountModel: DatabaseModelInterface { typealias Module = UserModule struct FieldKeys { struct v1 { static var email: FieldKey { "email" } static var password: FieldKey { "password" } } } @ID() var id: UUID? @Field(key: FieldKeys.v1.email) var email: String @Field(key: FieldKeys.v1.password) var password: String init() { } init( id: UUID? = nil, email: String, password: String ) { self.id = id self.email = email self.password = password } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/User/Database/Models/UserTokenModel.swift ================================================ import Vapor import Fluent final class UserTokenModel: DatabaseModelInterface { typealias Module = UserModule struct FieldKeys { struct v1 { static var value: FieldKey { "value" } static var userId: FieldKey { "user_id" } } } @ID() var id: UUID? @Field(key: FieldKeys.v1.value) var value: String @Parent(key: FieldKeys.v1.userId) var user: UserAccountModel init() { } init( id: UUID? = nil, value: String, userId: UUID ) { self.id = id self.value = value self.$user.id = userId } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/User/Forms/UserLoginForm.swift ================================================ import Vapor final class UserLoginForm: AbstractForm { public convenience init() { self.init( action: .init( method: .post, url: "/sign-in/" ), submit: "Sign in" ) self.fields = createFields() } @FormComponentBuilder func createFields() -> [FormComponent] { InputField("email") .config { $0.output.context.label.required = true $0.output.context.type = .email } .validators { FormFieldValidator.required($1) FormFieldValidator.email($1) } InputField("password") .config { $0.output.context.label.required = true $0.output.context.type = .password } .validators { FormFieldValidator.required($1) } } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/User/Objects/User.swift ================================================ enum User: ApiModuleInterface { enum Account: ApiModelInterface { typealias Module = User } enum Token: ApiModelInterface { typealias Module = User } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/User/Objects/UserAccount.swift ================================================ import Foundation extension User.Account { struct List: Codable { let id: UUID let email: String } struct Detail: Codable { let id: UUID let email: String } struct Create: Codable { let email: String let password: String } struct Update: Codable { let email: String let password: String? } struct Patch: Codable { let email: String? let password: String? } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/User/Objects/UserToken.swift ================================================ import Foundation extension User.Token { struct Detail: Codable { let id: UUID let value: String let user: User.Account.Detail } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/User/Templates/Contexts/UserLoginContext.swift ================================================ struct UserLoginContext { let icon: String let title: String let message: String let form: TemplateRepresentable init( icon: String, title: String, message: String, form: TemplateRepresentable ) { self.icon = icon self.title = title self.message = message self.form = form } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/User/Templates/Html/UserLoginTemplate.swift ================================================ import Vapor import SwiftHtml struct UserLoginTemplate: TemplateRepresentable { var context: UserLoginContext init( _ context: UserLoginContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") context.form.render(req) } .id("user-login") .class("container") } .render(req) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/User/UserModule.swift ================================================ import Vapor struct UserModule: ModuleInterface { let router = UserRouter() func boot(_ app: Application) throws { app.migrations.add(UserMigrations.v1()) app.migrations.add(UserMigrations.seed()) app.middleware.use(UserSessionAuthenticator()) app.middleware.use(UserTokenAuthenticator()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/User/UserRouter.swift ================================================ import Vapor struct UserRouter: RouteCollection { let frontendController = UserFrontendController() let apiController = UserApiController() func boot(routes: RoutesBuilder) throws { routes.get("sign-in", use: frontendController.signInView) routes .grouped(UserCredentialsAuthenticator()) .post("sign-in", use: frontendController.signInAction) routes.get("sign-out", use: frontendController.signOut) routes.grouped("api") .grouped(UserCredentialsAuthenticator()) .post("sign-in", use: apiController.signInApi) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Web/Controllers/WebFrontendController.swift ================================================ import Vapor struct WebFrontendController { func homeView(req: Request) throws -> Response { let ctx = WebHomeContext( icon: "👋", title: "Home", message: "Hi there, welcome to my page.", paragraphs: [ "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.", "Nisi ut aliquip ex ea commodo consequat.", ], link: .init( label: "Read my blog →", url: "/blog/" ) ) return req.templates.renderHtml( WebHomeTemplate(ctx) ) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Web/Templates/Contexts/WebHomeContext.swift ================================================ struct WebHomeContext { let icon: String let title: String let message: String let paragraphs: [String] let link: WebLinkContext } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Web/Templates/Contexts/WebIndexContext.swift ================================================ public struct WebIndexContext { public let title: String public init( title: String ) { self.title = title } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Web/Templates/Contexts/WebLinkContext.swift ================================================ public struct WebLinkContext { public let label: String public let url: String public init( label: String, url: String ) { self.label = label self.url = url } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Web/Templates/Html/WebHomeTemplate.swift ================================================ import Vapor import SwiftHtml struct WebHomeTemplate: TemplateRepresentable { var context: WebHomeContext init( _ context: WebHomeContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") for paragraph in context.paragraphs { P(paragraph) } WebLinkTemplate(context.link).render(req) } .id("home") .class("container") } .render(req) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg public struct WebIndexTemplate: TemplateRepresentable { public var context: WebIndexContext var body: Tag public init( _ context: WebIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Link(rel: .shortcutIcon) .href("/images/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css") Link(rel: .stylesheet) .href("/css/web.css") Title(context.title) } Body { Header { Div { A { Img(src: "/img/logo.png", alt: "Logo") } .id("site-logo") .href("/") Nav { Input() .type(.checkbox) .id("primary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("primary-menu-button") Div { A("Home") .href("/") .class("selected", req.url.path == "/") A("Blog") .href("/blog/") .class("selected", req.url.path == "/blog/") A("About") .href("#") .onClick("javascript:about();") if req.auth.has(AuthenticatedUser.self) { A("Admin") .href("/admin/") A("Sign out") .href("/sign-out/") } else { A("Sign in") .href("/sign-in/") } } .class("menu-items") } .id("primary-menu") } .id("navigation") } Main { body } Footer { Section { P { Text("This site is powered by ") A("Swift") .href("https://swift.org") .target(.blank) Text(" & ") A("Vapor") .href("https://vapor.codes") .target(.blank) Text(".") } P("myPage © 2020-2022") } } Script() .type(.javascript) .src("/js/web.js") } } .lang("en-US") } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Web/Templates/Html/WebLinkTemplate.swift ================================================ import Vapor import SwiftHtml struct WebLinkTemplate: TemplateRepresentable { var context: WebLinkContext init( _ context: WebLinkContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { A(context.label) .href(context.url) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Web/WebModule.swift ================================================ import Vapor struct WebModule: ModuleInterface { let router = WebRouter() func boot(_ app: Application) throws { try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Modules/Web/WebRouter.swift ================================================ import Vapor struct WebRouter: RouteCollection { let frontendController = WebFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get(use: frontendController.homeView) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Template/Request+Template.swift ================================================ import Vapor public extension Request { var templates: TemplateRenderer { .init(self) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Template/TemplateRenderer.swift ================================================ import Vapor import SwiftHtml public struct TemplateRenderer { var req: Request init(_ req: Request) { self.req = req } public func renderHtml( _ template: TemplateRepresentable, minify: Bool = false, indent: Int = 4 ) -> Response { let doc = Document(.html) { template.render(req) } let body = DocumentRenderer( minify: minify, indent: indent ) .render(doc) return Response( status: .ok, headers: [ "content-type": "text/html" ], body: .init(string: body) ) } } ================================================ FILE: Chapter 13/myProject/Sources/App/Template/TemplateRepresentable.swift ================================================ /// FILE: Sources/App/Template/TemplateRepresentable.swift import Vapor import SwiftSgml public protocol TemplateRepresentable { @TagBuilder func render(_ req: Request) -> Tag } ================================================ FILE: Chapter 13/myProject/Sources/App/configure.swift ================================================ import Vapor import Fluent import FluentSQLiteDriver import Liquid import LiquidLocalDriver public func configure( _ app: Application ) throws { app.fileStorages.use( .local( publicUrl: "http://localhost:8080", publicPath: app.directory.publicDirectory, workDirectory: "assets" ), as: .local ) app.routes.defaultMaxBodySize = "10mb" let dbPath = app.directory.resourcesDirectory + "db.sqlite" app.databases.use(.sqlite(.file(dbPath)), as: .sqlite) app.middleware.use( FileMiddleware( publicDirectory: app.directory.publicDirectory ) ) app.middleware.use(ExtendPathMiddleware()) app.sessions.use(.fluent) app.migrations.add(SessionRecord.migration) app.middleware.use(app.sessions.middleware) let modules: [ModuleInterface] = [ WebModule(), UserModule(), AdminModule(), ApiModule(), BlogModule(), ] for module in modules { try module.boot(app) } try app.autoMigrate().wait() } ================================================ FILE: Chapter 13/myProject/Sources/App/routes.swift ================================================ import Vapor import SwiftHtml func routes(_ app: Application) throws { app.routes.get { req -> Response in req.templates.renderHtml( WebIndexTemplate( WebIndexContext( title: "Home" ) ) { P("Hi there, welcome to my page!") } ) } } ================================================ FILE: Chapter 13/myProject/Sources/Run/main.swift ================================================ import App import Vapor var env = try Environment.detect() try LoggingSystem.bootstrap(from: &env) let app = Application(env) defer { app.shutdown() } try configure(app) try app.run() ================================================ FILE: Chapter 13/myProject/Tests/AppTests/AppTests.swift ================================================ @testable import App import XCTVapor final class AppTests: XCTestCase { func testHelloWorld() throws { let app = Application(.testing) defer { app.shutdown() } try configure(app) try app.test(.GET, "hello", afterResponse: { res in XCTAssertEqual(res.status, .ok) XCTAssertEqual(res.body.string, "Hello, world!") }) } } ================================================ FILE: Chapter 13/myProject/docker-compose.yml ================================================ # 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 # Stop all: docker compose down # x-shared_environment: &shared_environment LOG_LEVEL: ${LOG_LEVEL:-debug} services: app: image: myproject:latest build: context: . environment: <<: *shared_environment 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"] ================================================ FILE: Chapter 14/myProject/Dockerfile ================================================ # ================================ # Build image # ================================ FROM swift:6.0-noble 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 \ $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true) # Copy entire repo into container COPY . . # Build the application, 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 \ --product myProject \ --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)/myProject" ./ # 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:noble # 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 ["./myProject"] CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] ================================================ FILE: Chapter 14/myProject/Package.swift ================================================ // swift-tools-version:5.7 import PackageDescription let package = Package( name: "myProject", platforms: [ .macOS(.v12) ], dependencies: [ .package( url: "https://github.com/vapor/vapor", from: "4.70.0" ), .package( url: "https://github.com/vapor/fluent", from: "4.4.0" ), .package( url: "https://github.com/vapor/fluent-sqlite-driver", from: "4.1.0" ), .package( url: "https://github.com/binarybirds/liquid", from: "1.3.0" ), .package( url: "https://github.com/binarybirds/liquid-local-driver", from: "1.3.0" ), .package( url: "https://github.com/binarybirds/swift-html", from: "1.7.0" ), .package( url: "https://github.com/binarybirds/spec", from: "1.2.0" ), ], targets: [ .target(name: "App", dependencies: [ .product(name: "Vapor", package: "vapor"), .product(name: "Fluent", package: "fluent"), .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), .product(name: "Liquid", package: "liquid"), .product(name: "LiquidLocalDriver", package: "liquid-local-driver"), .product(name: "SwiftHtml", package: "swift-html"), .product(name: "SwiftSvg", package: "swift-html"), ]), .executableTarget(name: "Run", dependencies: ["App"]), .testTarget(name: "AppTests", dependencies: [ .target(name: "App"), .product(name: "XCTVapor", package: "vapor"), .product(name: "Spec", package: "spec"), ]) ] ) ================================================ FILE: Chapter 14/myProject/Public/css/admin.css ================================================ tr { column-gap: 1rem; } ================================================ FILE: Chapter 14/myProject/Public/css/web.css ================================================ #blog h2 { margin: 0.5rem 0; } ================================================ FILE: Chapter 14/myProject/Public/js/admin.js ================================================ document.addEventListener("keydown", function(e) { if ( (window.navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey) && e.keyCode == 83 ) { e.preventDefault(); document.forms[0].submit(); } }, false); ================================================ FILE: Chapter 14/myProject/Public/js/web.js ================================================ function about() { alert("myPage\n\nversion 1.0.0"); } ================================================ FILE: Chapter 14/myProject/Sources/App/Extensions/Svg+MenuIcon.swift ================================================ import SwiftSvg extension Svg { static func menuIcon() -> Svg { Svg { Line(x1: 3, y1: 12, x2: 21, y2: 12) Line(x1: 3, y1: 6, x2: 21, y2: 6) Line(x1: 3, y1: 18, x2: 21, y2: 18) } .width(24) .height(24) .viewBox(minX: 0, minY: 0, width: 24, height: 24) .fill("none") .stroke("currentColor") .strokeWidth(2) .strokeLinecap("round") .strokeLinejoin("round") } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/ApiModelInterface.swift ================================================ import Vapor public protocol ApiModelInterface { associatedtype Module: ApiModuleInterface static var pathKey: String { get } static var pathIdKey: String { get } } extension ApiModelInterface { static var pathKey: String { String(describing: self).lowercased() + "s" } static var pathIdKey: String { String(describing: self).lowercased() + "Id" } static var pathIdComponent: PathComponent { .init(stringLiteral: ":" + pathIdKey) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/ApiModuleInterface.swift ================================================ import Foundation public protocol ApiModuleInterface { static var pathKey: String { get } } public extension ApiModuleInterface { static var pathKey: String { String(describing: self).lowercased() } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/AuthenticatedUser.swift ================================================ import Vapor public struct AuthenticatedUser { public let id: UUID public let email: String public init( id: UUID, email: String ) { self.id = id self.email = email } } extension AuthenticatedUser: SessionAuthenticatable { public var sessionID: UUID { id } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Controllers/CreateController.swift ================================================ import Vapor protocol CreateController: ModelController { func create( _ req: Request, _ model: DatabaseModel ) async throws func beforeCreate( _ req: Request, _ model: DatabaseModel ) async throws func afterCreate( _ req: Request, _ model: DatabaseModel ) async throws } extension CreateController { func create( _ req: Request, _ model: DatabaseModel ) async throws { try await beforeCreate(req, model) try await model.create(on: req.db) try await afterCreate(req, model) } func beforeCreate( _ req: Request, _ model: DatabaseModel ) async throws {} func afterCreate( _ req: Request, _ model: DatabaseModel ) async throws {} } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Controllers/DeleteController.swift ================================================ import Vapor public protocol DeleteController: ModelController { func delete( _ req: Request, _ model: DatabaseModel ) async throws func beforeDelete( _ req: Request, _ model: DatabaseModel ) async throws func afterDelete( _ req: Request, _ model: DatabaseModel ) async throws } public extension DeleteController { func delete( _ req: Request, _ model: DatabaseModel ) async throws { try await beforeDelete(req, model) try await model.delete(on: req.db) try await afterDelete(req, model) } func beforeDelete( _ req: Request, _ model: DatabaseModel ) async throws {} func afterDelete( _ req: Request, _ model: DatabaseModel ) async throws {} } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Controllers/DetailController.swift ================================================ import Vapor import Fluent protocol DetailController: ModelController { func detail( _ req: Request ) async throws -> DatabaseModel func beforeDetail( _ req: Request, _ queryBuilder: QueryBuilder ) async throws -> QueryBuilder func afterDetail( _ req: Request, _ model: DatabaseModel ) async throws -> DatabaseModel } extension DetailController { func detail( _ req: Request ) async throws -> DatabaseModel { let queryBuilder = DatabaseModel.query(on: req.db) let model = try await beforeDetail(req, queryBuilder) .filter(\._$id == identifier(req)) .first() guard let model = model else { throw Abort(.notFound) } return try await afterDetail(req, model) } func beforeDetail( _ req: Request, _ queryBuilder: QueryBuilder ) async throws -> QueryBuilder { queryBuilder } func afterDetail( _ req: Request, _ model: DatabaseModel ) async throws -> DatabaseModel { model } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Controllers/ListController.swift ================================================ import Vapor import Fluent protocol ListController: ModelController { func list( _ req: Request ) async throws -> [DatabaseModel] func beforeList( _ req: Request, _ queryBuilder: QueryBuilder ) async throws -> QueryBuilder func afterList( _ req: Request, _ models: [DatabaseModel] ) async throws -> [DatabaseModel] } extension ListController { func list( _ req: Request ) async throws -> [DatabaseModel] { try await DatabaseModel .query(on: req.db) .all() } func beforeList( _ req: Request, _ queryBuilder: QueryBuilder ) async throws -> QueryBuilder { queryBuilder } func afterList( _ req: Request, _ models: [DatabaseModel] ) async throws -> [DatabaseModel] { models } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Controllers/ModelController.swift ================================================ import Vapor import Fluent public struct Name { let singular: String let plural: String init( singular: String, plural: String? = nil ) { self.singular = singular self.plural = plural ?? singular + "s" } } public protocol ModelController { associatedtype ApiModel: ApiModelInterface associatedtype DatabaseModel: DatabaseModelInterface static var moduleName: String { get } static var modelName: Name { get } func identifier( _ req: Request ) throws -> UUID func findBy( _ id: UUID, on: Database ) async throws -> DatabaseModel func getBaseRoutes( _ routes: RoutesBuilder ) -> RoutesBuilder } extension ModelController { static var moduleName: String { DatabaseModel.Module.identifier.capitalized } static var modelName: Name { .init(singular: String(DatabaseModel.identifier.dropLast(1))) } func identifier( _ req: Request ) throws -> UUID { guard let id = req.parameters.get(ApiModel.pathIdKey), let uuid = UUID(uuidString: id) else { throw Abort(.badRequest) } return uuid } func findBy( _ id: UUID, on db: Database ) async throws -> DatabaseModel { guard let model = try await DatabaseModel.find(id, on: db) else { throw Abort(.notFound) } return model } func getBaseRoutes( _ routes: RoutesBuilder ) -> RoutesBuilder { routes .grouped(ApiModel.Module.pathKey.pathComponents) .grouped(ApiModel.pathKey.pathComponents) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Controllers/PatchController.swift ================================================ import Vapor public protocol PatchController: ModelController { func patch( _ req: Request, _ model: DatabaseModel ) async throws func beforePatch( _ req: Request, _ model: DatabaseModel ) async throws func afterPatch( _ req: Request, _ model: DatabaseModel ) async throws } public extension PatchController { func patch( _ req: Request, _ model: DatabaseModel ) async throws { try await beforePatch(req, model) try await model.update(on: req.db) try await afterPatch(req, model) } func beforePatch( _ req: Request, _ model: DatabaseModel ) async throws {} func afterPatch( _ req: Request, _ model: DatabaseModel ) async throws {} } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Controllers/UpdateController.swift ================================================ import Vapor public protocol UpdateController: ModelController { func update( _ req: Request, _ model: DatabaseModel ) async throws func beforeUpdate( _ req: Request, _ model: DatabaseModel ) async throws func afterUpdate( _ req: Request, _ model: DatabaseModel ) async throws } public extension UpdateController { func update( _ req: Request, _ model: DatabaseModel ) async throws { try await beforeUpdate(req, model) try await model.update(on: req.db) try await afterUpdate(req, model) } func beforeUpdate( _ req: Request, _ model: DatabaseModel ) async throws {} func afterUpdate( _ req: Request, _ model: DatabaseModel ) async throws {} } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/DatabaseModelInterface.swift ================================================ import Vapor import Fluent public protocol DatabaseModelInterface: Fluent.Model where Self.IDValue == UUID { associatedtype Module: ModuleInterface static var identifier: String { get } } public extension DatabaseModelInterface { static var schema: String { Module.identifier + "_" + identifier } static var identifier: String { String(describing: self) .dropFirst(Module.identifier.count) .dropLast(5) .lowercased() + "s" } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Extensions/ByteBuffer+Data.swift ================================================ import Vapor public extension ByteBuffer { var data: Data? { getData(at: 0, length: readableBytes) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Extensions/File+ByteBuffer.swift ================================================ import Vapor public extension File { var byteBuffer: ByteBuffer { data } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Form/AbstractForm.swift ================================================ import Vapor open class AbstractForm: FormComponent { open var action: FormAction open var fields: [FormComponent] open var error: String? open var submit: String? public init( action: FormAction = .init(), fields: [FormComponent] = [], error: String? = nil, submit: String? = nil ) { self.action = action self.fields = fields self.error = error self.submit = submit self.action.enctype = .multipart } open func load(req: Request) async throws { for field in fields { try await field.load(req: req) } } open func process(req: Request) async throws { for field in fields { try await field.process(req: req) } } open func validate(req: Request) async throws -> Bool { var result: [Bool] = [] for field in fields { result.append(try await field.validate(req: req)) } return result.filter { $0 == false }.isEmpty } open func write(req: Request) async throws { for field in fields { try await field.write(req: req) } } open func save(req: Request) async throws { for field in fields { try await field.save(req: req) } } open func read(req: Request) async throws { for field in fields { try await field.read(req: req) } } open func render(req: Request) -> TemplateRepresentable { FormTemplate(getContext(req)) } func getContext(_ req: Request) -> FormContext { .init( action: action, fields: fields.map { $0.render(req: req)}, error: error, submit: submit ) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Form/AbstractFormField.swift ================================================ import Vapor open class AbstractFormField< Input: Decodable, Output: TemplateRepresentable >: FormComponent { public var key: String public var input: Input public var output: Output public var error: String? // MARK: - event blocks public typealias FormFieldBlock = (Request, AbstractFormField) async throws -> Void public typealias FormFieldValidatorsBlock = ((Request, AbstractFormField) -> [AsyncValidator]) private var readBlock: FormFieldBlock? private var writeBlock: FormFieldBlock? private var loadBlock: FormFieldBlock? private var saveBlock: FormFieldBlock? private var validatorsBlock: FormFieldValidatorsBlock? // MARK: - init & config public init( key: String, input: Input, output: Output, error: String? = nil ) { self.key = key self.input = input self.output = output self.error = error } open func config( _ block: (AbstractFormField) -> Void ) -> Self { block(self) return self } // MARK: - Block setters open func read(_ block: @escaping FormFieldBlock) -> Self { readBlock = block return self } open func write(_ block: @escaping FormFieldBlock) -> Self { writeBlock = block return self } open func load(_ block: @escaping FormFieldBlock) -> Self { loadBlock = block return self } open func save(_ block: @escaping FormFieldBlock) -> Self { saveBlock = block return self } open func validators( @AsyncValidatorBuilder _ block: @escaping FormFieldValidatorsBlock ) -> Self { validatorsBlock = block return self } // MARK: - FormComponent open func process(req: Request) async throws { if let value = try? req.content.get(Input.self, at: key) { input = value } } open func validate(req: Request) async throws -> Bool { guard let validators = validatorsBlock else { return true } return await RequestValidator(validators(req, self)).isValid(req) } open func read(req: Request) async throws { try await readBlock?(req, self) } open func write(req: Request) async throws { try await writeBlock?(req, self) } open func load(req: Request) async throws { try await loadBlock?(req, self) } open func save(req: Request) async throws { try await saveBlock?(req, self) } open func render(req: Request) -> TemplateRepresentable { output } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Form/Fields/HiddenField.swift ================================================ import Vapor public final class HiddenField: AbstractFormField< String, HiddenFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Form/Fields/ImageField.swift ================================================ import Vapor public final class ImageField: AbstractFormField< FormImageInput, ImageFieldTemplate > { public var imageKey: String? { didSet { output.context.data.originalKey = imageKey } } public var path: String public init( _ key: String, path: String ) { self.path = path super.init( key: key, input: .init(key: key), output: .init(.init(key: key)) ) } public override func process( req: Request ) async throws { /// process input input.file = try? req.content.get( File.self, at: key ) input.data.originalKey = try? req.content.get( String.self, at: key + "OriginalKey" ) if let temporaryFileKey = try? req.content.get( String.self, at: key + "TemporaryFileKey" ), let temporaryFileName = try? req.content.get( String.self, at: key + "TemporaryFileName" ) { input.data.temporaryFile = .init( key: temporaryFileKey, name: temporaryFileName ) } input.data.shouldRemove = (try? req.content.get( Bool.self, at: key + "ShouldRemove" )) ?? false /// remove & upload file if input.data.shouldRemove { if let originalKey = input.data.originalKey { try? await req.fs.delete(key: originalKey) } } else if let file = input.file, let data = file.byteBuffer.data, !data.isEmpty { if let tmpKey = input.data.temporaryFile?.key { try? await req.fs.delete(key: tmpKey) } let key = "tmp/\(UUID().uuidString).tmp" _ = try await req.fs.upload(key: key, data: data) /// update the temporary image input.data.temporaryFile = .init( key: key, name: file.filename ) } /// update output values output.context.data = input.data } public override func write( req: Request ) async throws { imageKey = input.data.originalKey if input.data.shouldRemove { if let key = input.data.originalKey { try? await req.fs.delete(key: key) } imageKey = nil } else if let file = input.data.temporaryFile { var newKey = path + "/" + file.name if await req.fs.exists(key: newKey) { let formatter = DateFormatter() formatter.dateFormat="y-MM-dd-HH-mm-ss-" let prefix = formatter.string(from: .init()) newKey = path + "/" + prefix + file.name } _ = try await req.fs.move(key: file.key, to: newKey) input.data.temporaryFile = nil if let key = input.data.originalKey { try? await req.fs.delete(key: key) } imageKey = newKey } try await super.write(req: req) } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Form/Fields/InputField.swift ================================================ public final class InputField: AbstractFormField< String, InputFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init( key: key ) ) ) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Form/Fields/SelectField.swift ================================================ import Vapor public final class SelectField: AbstractFormField< String, SelectFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Form/Fields/TextareaField.swift ================================================ import Vapor public final class TextareaField: AbstractFormField< String, TextareaFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Form/FormAction.swift ================================================ import SwiftHtml public struct FormAction { public var method: SwiftHtml.Method public var url: String? public var enctype: SwiftHtml.Enctype? public init( method: SwiftHtml.Method = .post, url: String? = nil, enctype: SwiftHtml.Enctype? = nil ) { self.method = method self.url = url self.enctype = enctype } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Form/FormComponent.swift ================================================ import Vapor public protocol FormComponent { func load(req: Request) async throws func process(req: Request) async throws func validate(req: Request) async throws -> Bool func write(req: Request) async throws func save(req: Request) async throws func read(req: Request) async throws func render(req: Request) -> TemplateRepresentable } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Form/FormComponentBuilder.swift ================================================ @resultBuilder public enum FormComponentBuilder { public static func buildBlock( _ components: FormComponent... ) -> [FormComponent] { components } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Form/FormImageData.swift ================================================ public struct FormImageData: Codable { public struct TemporaryFile: Codable { public let key: String public let name: String public init( key: String, name: String ) { self.key = key self.name = name } } public var originalKey: String? public var temporaryFile: TemporaryFile? public var shouldRemove: Bool public init( originalKey: String? = nil, temporaryFile: TemporaryFile? = nil, shouldRemove: Bool = false ) { self.originalKey = originalKey self.temporaryFile = temporaryFile self.shouldRemove = shouldRemove } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Form/FormImageInput.swift ================================================ import Vapor public struct FormImageInput: Codable { public var key: String public var file: File? public var data: FormImageData public init( key: String, file: File? = nil, data: FormImageData? = nil ) { self.key = key self.file = file self.data = data ?? .init() } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/ModelEditorInterface.swift ================================================ import Vapor public protocol ModelEditorInterface: FormComponent { associatedtype Model: DatabaseModelInterface var model: Model { get } var form: AbstractForm { get } init(model: Model, form: AbstractForm) @FormComponentBuilder var formFields: [FormComponent] { get } } public extension ModelEditorInterface { func load(req: Request) async throws { try await form.load(req: req) } func process(req: Request) async throws { try await form.process(req: req) } func validate(req: Request) async throws -> Bool { try await form.validate(req: req) } func write(req: Request) async throws { try await form.write(req: req) } func save(req: Request) async throws { try await form.save(req: req) } func read(req: Request) async throws { try await form.read(req: req) } func render(req: Request) -> TemplateRepresentable { form.render(req: req) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/ModuleInterface.swift ================================================ import Vapor public protocol ModuleInterface { static var identifier: String { get } func boot(_ app: Application) throws } public extension ModuleInterface { func boot(_ app: Application) throws {} static var identifier: String { String(describing: self).dropLast(6).lowercased() } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Contexts/CellContext.swift ================================================ public struct CellContext { public enum `Type`: String { case text case image } public let value: String public let link: LinkContext? public let type: `Type` public init( _ value: String, link: LinkContext? = nil, type: `Type` = .text ) { self.type = type self.value = value self.link = link } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Contexts/ColumnContext.swift ================================================ public struct ColumnContext { public let key: String public let label: String public init( _ key: String, label: String? = nil ) { self.key = key self.label = label ?? key.capitalized } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Contexts/DetailContext.swift ================================================ public struct DetailContext { public enum `Type`: String { case text case image } public let key: String public let label: String public let value: String public let type: `Type` public init( _ key: String, _ value: String, label: String? = nil, type: `Type` = .text ) { self.key = key self.label = label ?? key.capitalized self.value = value self.type = type } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Contexts/FormContext.swift ================================================ public struct FormContext { public var action: FormAction public var fields: [TemplateRepresentable] public var error: String? public var submit: String? } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Contexts/HiddenFieldContext.swift ================================================ public struct HiddenFieldContext { public let key: String public var value: String? public init( key: String, value: String? = nil ) { self.key = key self.value = value } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Contexts/ImageFieldContext.swift ================================================ public struct ImageFieldContext { public let key: String public var label: LabelContext public var data: FormImageData public var previewUrl: String? public var accept: String? public var error: String? public init( key: String, label: LabelContext? = nil, data: FormImageData = .init(), previewUrl: String? = nil, accept: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.data = data self.previewUrl = previewUrl self.accept = accept self.error = error } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Contexts/InputFieldContext.swift ================================================ import SwiftHtml public struct InputFieldContext { public let key: String public var label: LabelContext public var type: SwiftHtml.Input.`Type` public var placeholder: String? public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, type: Input.`Type` = .text, placeholder: String? = nil, value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.type = type self.placeholder = placeholder self.value = value self.error = error } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Contexts/LabelContext.swift ================================================ public struct LabelContext { public var key: String public var title: String? public var required: Bool public var more: String? public init( key: String, title: String? = nil, required: Bool = false, more: String? = nil ) { self.key = key self.title = title self.required = required self.more = more } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Contexts/LinkContext.swift ================================================ import Vapor public struct LinkContext { public let label: String public let path: String public let absolute: Bool public let isBlank: Bool public let dropLast: Int public init( label: String, path: String = "", absolute: Bool = false, isBlank: Bool = false, dropLast: Int = 0 ) { self.label = label self.path = path self.absolute = absolute self.isBlank = isBlank self.dropLast = dropLast } public func url( _ req: Request, _ infix: [PathComponent] = [] ) -> String { if absolute { return path } return "/" + (req.url.path.pathComponents.dropLast(dropLast) + (infix + path.pathComponents)).string } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Contexts/OptionContext.swift ================================================ public struct OptionContext { public var key: String public var label: String public init( key: String, label: String ) { self.key = key self.label = label } } public extension OptionContext { static func yesNo() -> [OptionContext] { ["yes", "no"].map { .init(key: $0, label: $0.capitalized) } } static func trueFalse() -> [OptionContext] { [true, false].map { .init(key: String($0), label: String($0).capitalized) } } static func numbers( _ numbers: [Int] ) -> [OptionContext] { numbers.map { .init(key: String($0), label: String($0)) } } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Contexts/RowContext.swift ================================================ public struct RowContext { public let id: String public let cells: [CellContext] public init( id: String, cells: [CellContext] ) { self.id = id self.cells = cells } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Contexts/SelectFieldContext.swift ================================================ public struct SelectFieldContext { public let key: String public var label: LabelContext public var options: [OptionContext] public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, options: [OptionContext] = [], value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.options = options self.value = value self.error = error } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Contexts/TableContext.swift ================================================ public struct TableContext { public let columns: [ColumnContext] public let rows: [RowContext] public let actions: [LinkContext] public init( columns: [ColumnContext], rows: [RowContext], actions: [LinkContext] = [] ) { self.columns = columns self.rows = rows self.actions = actions } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Contexts/TextareaFieldContext.swift ================================================ public struct TextareaFieldContext { public let key: String public var label: LabelContext public var placeholder: String? public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, placeholder: String? = nil, value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.placeholder = placeholder self.value = value self.error = error } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Html/CellTemplate.swift ================================================ import Vapor import SwiftHtml public struct CellTemplate: TemplateRepresentable { var context: CellContext var rowId: String public init( _ context: CellContext, rowId: String ) { self.context = context self.rowId = rowId } @TagBuilder public func render( _ req: Request ) -> Tag { Td { switch context.type { case .text: if let link = context.link { LinkTemplate(link, pathInfix: rowId).render(req) } else { Text(context.value) } case .image: if let link = context.link { LinkTemplate(link, pathInfix: rowId) { label in Img(src: context.value, alt: label) } .render(req) } else { Img(src: context.value, alt: context.value) } } } .class("field") } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Html/DetailTemplate.swift ================================================ import Vapor import SwiftHtml public struct DetailTemplate: TemplateRepresentable { var context: DetailContext public init( _ context: DetailContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Dt(context.label) switch context.type { case .text: if context.value.isEmpty { Dd(" ") } else { Dd( context.value.replacingOccurrences( of: "\n", with: "
" ) ) } case .image: Dd { Img( src: context.value, alt: context.label ) } } } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Html/FormTemplate.swift ================================================ import Vapor import SwiftHtml public struct FormTemplate: TemplateRepresentable { var context: FormContext public init( _ context: FormContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Form { if let error = context.error { Section { P(error) .class("error") } } for field in context.fields { Section { field.render(req) } } Section { Input() .type(.submit) .value(context.submit ?? "Save") } } .method(context.action.method) .action(context.action.url) .enctype(context.action.enctype) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Html/HiddenFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct HiddenFieldTemplate: TemplateRepresentable { var context: HiddenFieldContext public init( _ context: HiddenFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Input() .type(.hidden) .name(context.key) .value(context.value) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Html/ImageFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct ImageFieldTemplate: TemplateRepresentable { public var context: ImageFieldContext public init( _ context: ImageFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { if let url = context.previewUrl { Img(src: req.fs.resolve(key: url), alt: context.key) } else { Img(src: "/img/logo.png", alt: "default post image") } LabelTemplate(context.label).render(req) Input() .type(.file) .key(context.key) .class("field") .accept(context.accept) if let temporaryFile = context.data.temporaryFile { Input() .key(context.key + "TemporaryFileKey") .value(temporaryFile.key) .type(.hidden) Input() .key(context.key + "TemporaryFileName") .value(temporaryFile.name) .type(.hidden) } if let key = context.data.originalKey { Input() .key(context.key + "OriginalKey") .value(key) .type(.hidden) } if !context.label.required { Input() .key(context.key + "ShouldRemove") .value(String(true)) .type(.checkbox) .checked(context.data.shouldRemove) Label("Remove") .for(context.key + "Remove") } if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Html/InputFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct InputFieldTemplate: TemplateRepresentable { public var context: InputFieldContext public init( _ context: InputFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Input() .type(context.type) .key(context.key) .placeholder(context.placeholder) .value(context.value) .class("field") if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Html/LabelTemplate.swift ================================================ import Vapor import SwiftHtml public struct LabelTemplate: TemplateRepresentable { var context: LabelContext public init( _ context: LabelContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Label { Text(context.title ?? context.key.capitalized) if let more = context.more { Span(more) .class("more") } if context.required { Span("*") .class("required") } }.for(context.key) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Html/LinkTemplate.swift ================================================ import Vapor import SwiftHtml public struct LinkTemplate: TemplateRepresentable { var context: LinkContext var body: Tag var pathInfix: String? public init( _ context: LinkContext, pathInfix: String? = nil, _ builder: ((String) -> Tag)? = nil ) { self.context = context self.pathInfix = pathInfix self.body = builder?(context.label) ?? Text(context.label) } @TagBuilder public func render( _ req: Request ) -> Tag { A { body } .href(context.url(req, pathInfix?.pathComponents ?? [])) .target(.blank, context.isBlank) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Html/SelectFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct SelectFieldTemplate: TemplateRepresentable { public var context: SelectFieldContext public init( _ context: SelectFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Select { for item in context.options { Option(item.label) .value(item.key) .selected(context.value == item.key) } } .name(context.key) if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Html/TableTemplate.swift ================================================ import Vapor import SwiftHtml public struct TableTemplate: TemplateRepresentable { var context: TableContext public init( _ context: TableContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Table { Thead { Tr { context.columns.map { column in Th(column.label) .id(column.key) .class("field") } context.actions.map { action in Th(action.label) .class("action") } } } Tbody { for row in context.rows { Tr { row.cells.map { CellTemplate($0, rowId: row.id).render(req) } context.actions.map { action in Td { LinkTemplate(action, pathInfix: row.id).render(req) } .class("action") } } .id(row.id) } } } } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Templates/Html/TextareaFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct TextareaFieldTemplate: TemplateRepresentable { public var context: TextareaFieldContext public init( _ context: TextareaFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Textarea(context.value) .placeholder(context.placeholder) .name(context.key) if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Validation/AsyncValidator.swift ================================================ import Vapor public protocol AsyncValidator { var key: String { get } var message: String { get } func validate( _ req: Request ) async throws -> ValidationErrorDetail? } public extension AsyncValidator { var error: ValidationErrorDetail { .init(key: key, message: message) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Validation/AsyncValidatorBuilder.swift ================================================ @resultBuilder public enum AsyncValidatorBuilder { public static func buildBlock( _ components: AsyncValidator... ) -> [AsyncValidator] { components } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Validation/FormFieldValidator+Validations.swift ================================================ import Vapor public extension FormFieldValidator where Input == String { static func required( _ field: AbstractFormField, _ message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is required" return .init(field, msg) { _, field in !field.input.isEmpty } } static func min( _ field: AbstractFormField, length: Int, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)" return .init(field, msg) { _, field in field.input.count >= length } } static func max( _ field: AbstractFormField, length: Int, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)" return .init(field, msg) { _, field in field.input.count <= length } } static func alphanumeric( _ field: AbstractFormField, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) should be only alphanumeric characters" return .init(field, msg) { _, field in !Validator .characterSet(.alphanumerics) .validate(field.input) .isFailure } } static func email( _ field: AbstractFormField, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) should be a valid email address" return .init(field, msg) { _, field in !Validator .email .validate(field.input) .isFailure } } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Validation/FormFieldValidator.swift ================================================ import Vapor public struct FormFieldValidator< Input: Decodable, Output: TemplateRepresentable >: AsyncValidator { public typealias AsyncValidationBlock = ((Request, AbstractFormField) async throws -> Bool) public let field: AbstractFormField public let message: String public let validation: AsyncValidationBlock public var key: String { field.key } public init( _ field: AbstractFormField, _ message: String, _ validation: @escaping AsyncValidationBlock ) { self.field = field self.message = message self.validation = validation } public func validate( _ req: Request ) async throws -> ValidationErrorDetail? { let isValid = try await validation(req, field) if isValid { return nil } field.error = message return error } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Validation/KeyedContentValidator+Validations.swift ================================================ import Vapor public extension KeyedContentValidator where T == String { static func required( _ key: String, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is required" return .init(key, msg, optional: optional) { value, _ in !value.isEmpty } } static func min( _ key: String, _ length: Int, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is too short (min: \(length) characters)" return .init(key, msg, optional: optional) { value, _ in value.count >= length } } static func max( _ key: String, _ length: Int, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is too long (max: \(length) characters)" return .init(key, msg, optional: optional) { value, _ in value.count <= length } } static func alphanumeric( _ key: String, _ message: String? = nil, _ optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) should be only alphanumeric characters" return .init(key, msg, optional: optional) { value, _ in !Validator.characterSet(.alphanumerics).validate(value).isFailure } } static func email( _ key: String, _ message: String? = nil, _ optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) should be a valid email address" return .init(key, msg, optional: optional) { value, _ in !Validator.email.validate(value).isFailure } } } public extension KeyedContentValidator where T == Int { static func min( _ key: String, _ length: Int, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is too short (min: \(length) characters)" return .init(key, msg, optional: optional) { value, _ in value >= length } } static func max( _ key: String, _ length: Int, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is too long (max: \(length) characters)" return .init(key, msg, optional: optional) { value, _ in value <= length } } static func contains( _ key: String, _ values: [Int], _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is an invalid value" return .init(key, msg, optional: optional) { value, _ in values.contains(value) } } } public extension KeyedContentValidator where T == UUID { static func required( _ key: String, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is required" return .init(key, msg, optional: optional) { _, _ in true } } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Validation/KeyedContentValidator.swift ================================================ import Vapor public struct KeyedContentValidator: AsyncValidator { public let key: String public let message: String public let optional: Bool public let validation: (T, Request) async throws -> Bool public init( _ key: String, _ message: String, optional: Bool = false, _ validation: @escaping (T, Request) async throws -> Bool ) { self.key = key self.message = message self.optional = optional self.validation = validation } public func validate( _ req: Request ) async throws -> ValidationErrorDetail? { let optionalValue = try? req.content.get(T.self, at: key) if let value = optionalValue { return try await validation(value, req) ? nil : error } return optional ? nil : error } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Validation/RequestValidator.swift ================================================ import Vapor public struct RequestValidator { public var validators: [AsyncValidator] public init( _ validators: [AsyncValidator] ) { self.validators = validators } public func validate( _ req: Request, message: String? = nil ) async throws { var result: [ValidationErrorDetail] = [] for validator in validators { if result.contains(where: { $0.key == validator.key }) { continue } if let res = try await validator.validate(req) { result.append(res) } } if !result.isEmpty { throw ValidationAbort( abort: Abort(.badRequest, reason: message), details: result ) } } public func isValid( _ req: Request ) async -> Bool { do { try await validate(req, message: nil) return true } catch { return false } } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Validation/ValidationAbort.swift ================================================ import Vapor public struct ValidationAbort: AbortError { public var abort: Abort public var message: String? public var details: [ValidationErrorDetail] public var reason: String { abort.reason } public var status: HTTPStatus { abort.status } public init( abort: Abort, message: String? = nil, details: [ValidationErrorDetail] ) { self.abort = abort self.message = message self.details = details } } ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Validation/ValidationError.swift ================================================ import Vapor struct ValidationError: Codable { let message: String? let details: [ValidationErrorDetail] init( message: String?, details: [ValidationErrorDetail] ) { self.message = message self.details = details } } extension ValidationError: Content {} ================================================ FILE: Chapter 14/myProject/Sources/App/Framework/Validation/ValidationErrorDetail.swift ================================================ import Vapor public struct ValidationErrorDetail: Codable { public var key: String public var message: String public init( key: String, message: String ) { self.key = key self.message = message } } extension ValidationErrorDetail: Content {} ================================================ FILE: Chapter 14/myProject/Sources/App/Middlewares/ExtendPathMiddleware.swift ================================================ import Vapor struct ExtendPathMiddleware: AsyncMiddleware { func respond( to req: Request, chainingTo next: AsyncResponder ) async throws -> Response { if !req.url.path.hasSuffix("/") && !req.url.path.contains(".") { return req.redirect( to: req.url.path + "/", redirectType: .permanent ) } return try await next.respond( to: req ) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Admin/AdminModule.swift ================================================ import Vapor struct AdminModule: ModuleInterface { let router = AdminRouter() func boot(_ app: Application) throws { try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Admin/AdminRouter.swift ================================================ import Vapor struct AdminRouter: RouteCollection { let controller = AdminFrontendController() func boot(routes: RoutesBuilder) throws { routes .grouped( AuthenticatedUser.redirectMiddleware( path: "/sign-in/" ) ) .get("admin", use: controller.dashboardView) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Admin/Controllers/AdminController.swift ================================================ import Vapor protocol AdminController: AdminListController, AdminDetailController, AdminCreateController, AdminUpdateController, AdminDeleteController { func setupRoutes( _ routes: RoutesBuilder ) } extension AdminController { func setupRoutes( _ routes: RoutesBuilder ) { setupListRoutes(routes) setupDetailRoutes(routes) setupCreateRoutes(routes) setupUpdateRoutes(routes) setupDeleteRoutes(routes) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Admin/Controllers/AdminCreateController.swift ================================================ import Vapor protocol AdminCreateController: CreateController { associatedtype CreateModelEditor: ModelEditorInterface func createTemplate( _ req: Request, _ editor: CreateModelEditor ) -> TemplateRepresentable func createView( _ req: Request ) async throws -> Response func createAction( _ req: Request ) async throws -> Response func createContext( _ req: Request, _ editor: CreateModelEditor ) -> AdminEditorPageContext func createBreadcrumbs( _ req: Request ) -> [LinkContext] func setupCreateRoutes( _ routes: RoutesBuilder ) } extension AdminCreateController { private func render( _ req: Request, editor: CreateModelEditor ) -> Response { req.templates.renderHtml( createTemplate(req, editor) ) } func createView( _ req: Request ) async throws -> Response { let editor = CreateModelEditor( model: .init(), form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) return render(req, editor: editor) } func createAction( _ req: Request ) async throws -> Response { let model = DatabaseModel() let editor = CreateModelEditor( model: model as! CreateModelEditor.Model, form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) try await editor.process(req: req) let isValid = try await editor.validate(req: req) guard isValid else { return render(req, editor: editor) } try await editor.write(req: req) try await create(req, editor.model as! DatabaseModel) try await editor.save(req: req) var components = req.url.path.pathComponents.dropLast() components += editor.model.id!.uuidString.pathComponents return req.redirect(to: "/" + components.string + "/update/") } func createTemplate( _ req: Request, _ editor: CreateModelEditor ) -> TemplateRepresentable { AdminEditorPageTemplate( createContext(req, editor) ) } func createContext( _ req: Request, _ editor: CreateModelEditor ) -> AdminEditorPageContext { let context = FormContext( action: editor.form.action, fields: editor.form.fields.map { $0.render(req: req) }, error: editor.form.error, submit: editor.form.submit ) return .init( title: "Create", form: context, breadcrumbs: createBreadcrumbs(req) ) } func createBreadcrumbs( _ req: Request ) -> [LinkContext] { [ LinkContext( label: Self.moduleName.capitalized, dropLast: 2 ), LinkContext( label: Self.modelName.plural.capitalized, dropLast: 1 ), ] } func setupCreateRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) baseRoutes.get("create", use: createView) baseRoutes.post("create", use: createAction) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Admin/Controllers/AdminDeleteController.swift ================================================ import Vapor final class DeleteForm: AbstractForm { init() { super.init() self.action.method = .post self.submit = "Delete" } } protocol AdminDeleteController: DeleteController { func deleteView( _ req: Request ) async throws -> Response func deleteAction( _ req: Request ) async throws -> Response func deleteTemplate( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> TemplateRepresentable func deleteInfo( _ model: DatabaseModel ) -> String func deleteContext( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> AdminDeletePageContext func setupDeleteRoutes( _ routes: RoutesBuilder ) } extension AdminDeleteController { func deleteView( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let form = DeleteForm() return req.templates.renderHtml( deleteTemplate(req, model, form) ) } func deleteAction( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) try await delete(req, model) var url = req.url.path if let redirect = try? req.query.get(String.self, at: "redirect") { url = redirect } return req.redirect(to: url) } func deleteTemplate( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> TemplateRepresentable { AdminDeletePageTemplate( deleteContext(req, model, form) ) } func deleteContext( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> AdminDeletePageContext { .init( title: "Delete", name: deleteInfo(model), type: "model", form: form.getContext(req) ) } func setupDeleteRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.get("delete", use: deleteView) existingModelRoutes.post("delete", use: deleteAction) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Admin/Controllers/AdminDetailController.swift ================================================ import Vapor protocol AdminDetailController: DetailController { func detailView( _ req: Request ) async throws -> Response func detailTemplate( _ req: Request, _ model: DatabaseModel ) -> TemplateRepresentable func detailFields( for model: DatabaseModel ) -> [DetailContext] func detailContext( _ req: Request, _ model: DatabaseModel ) -> AdminDetailPageContext func detailBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func detailNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func setupDetailRoutes( _ routes: RoutesBuilder ) } extension AdminDetailController { func detailView( _ req: Request ) async throws -> Response { let model = try await detail(req) return req.templates.renderHtml(detailTemplate(req, model)) } func detailTemplate( _ req: Request, _ model: DatabaseModel ) -> TemplateRepresentable { AdminDetailPageTemplate(detailContext(req, model)) } func detailContext( _ req: Request, _ model: DatabaseModel ) -> AdminDetailPageContext { let path = "/delete/?redirect=" + req.url.path.pathComponents.dropLast().string + "&cancel=" + req.url.path return .init( title: "Details", fields: detailFields(for: model), navigation: detailNavigation(req, model), breadcrumbs: detailBreadcrumbs(req, model), actions: [ LinkContext( label: "Delete", path: path ), ] ) } func detailBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: Self.moduleName.capitalized, dropLast: 2 ), LinkContext( label: Self.modelName.plural.capitalized, dropLast: 1 ), ] } func detailNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: "Update", path: "update" ), ] } func setupDetailRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.get(use: detailView) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Admin/Controllers/AdminFrontendController.swift ================================================ import Vapor struct AdminFrontendController { func dashboardView( req: Request ) throws -> Response { let user = try req.auth.require(AuthenticatedUser.self) let template = AdminDashboardTemplate( .init( icon: "👋", title: "Dashboard", message: "Hello \(user.email), welcome to the CMS." ) ) return req.templates.renderHtml(template) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Admin/Controllers/AdminListController.swift ================================================ import Vapor protocol AdminListController: ListController { func listView( _ req: Request ) async throws -> Response func listColumns() -> [ColumnContext] func listCells( for model: DatabaseModel ) -> [CellContext] func listNavigation( _ req: Request ) -> [LinkContext] func listBreadcrumbs( _ req: Request ) -> [LinkContext] func listContext( _ req: Request, _ list: [DatabaseModel] ) -> AdminListPageContext func listTemplate( _ req: Request, _ list: [DatabaseModel] ) -> TemplateRepresentable func setupListRoutes( _ routes: RoutesBuilder ) } extension AdminListController { func listView(_ req: Request) async throws -> Response { let list = try await list(req) let template = listTemplate(req, list) return req.templates.renderHtml(template) } func listNavigation(_ req: Request) -> [LinkContext] { [ LinkContext(label: "Create",path: "create") ] } func listBreadcrumbs(_ req: Request) -> [LinkContext] { [ LinkContext( label: DatabaseModel.Module.identifier.capitalized, dropLast: 1 ) ] } func listContext( _ req: Request, _ list: [DatabaseModel] ) -> AdminListPageContext { let rows = list.map { RowContext(id: $0.id!.uuidString, cells: listCells(for: $0)) } let table = TableContext(columns: listColumns(), rows: rows, actions: [ LinkContext(label: "Update", path: "update"), LinkContext(label: "Delete", path: "delete") ]) return .init( title: "List", table: table, navigation: listNavigation(req), breadcrumbs: listBreadcrumbs(req) ) } func listTemplate( _ req: Request, _ list: [DatabaseModel] ) -> TemplateRepresentable { AdminListPageTemplate(listContext(req, list)) } func setupListRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) baseRoutes.get(use: listView) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Admin/Controllers/AdminUpdateController.swift ================================================ import Vapor protocol AdminUpdateController: UpdateController { associatedtype UpdateModelEditor: ModelEditorInterface func updateView( _ req: Request ) async throws -> Response func updateAction( _ req: Request ) async throws -> Response func updateTemplate( _ req: Request, _ editor: UpdateModelEditor ) async -> TemplateRepresentable func updateContext( _ req: Request, _ editor: UpdateModelEditor ) async -> AdminEditorPageContext func updateBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func updateNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func setupUpdateRoutes( _ routes: RoutesBuilder ) } extension AdminUpdateController { private func render( _ req: Request, editor: UpdateModelEditor ) async -> Response { req.templates.renderHtml( await updateTemplate(req, editor) ) } func updateView( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let editor = UpdateModelEditor( model: model as! UpdateModelEditor.Model, form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) try await editor.read(req: req) return await render(req, editor: editor) } func updateAction( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let editor = UpdateModelEditor( model: model as! UpdateModelEditor.Model, form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) try await editor.process(req: req) let isValid = try await editor.validate(req: req) guard isValid else { return await render(req, editor: editor) } try await editor.write(req: req) try await update(req, editor.model as! DatabaseModel) try await editor.save(req: req) return req.redirect(to: req.url.path) } func updateTemplate( _ req: Request, _ editor: UpdateModelEditor ) async -> TemplateRepresentable { await AdminEditorPageTemplate( updateContext(req, editor) ) } func updateContext( _ req: Request, _ editor: UpdateModelEditor ) async -> AdminEditorPageContext { let path = "delete/?redirect=" + req.url.path.pathComponents.dropLast(2).string + "&cancel=" + req.url.path let model = editor.model as! DatabaseModel return .init( title: "Update", form: editor.form.getContext(req), navigation: updateNavigation(req, model), breadcrumbs: updateBreadcrumbs(req, model), actions: [ LinkContext( label: "Delete", path: path, dropLast: 1 ), ] ) } func updateBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: Self.moduleName.capitalized, dropLast: 3 ), LinkContext( label: Self.modelName.plural.capitalized, dropLast: 2 ), ] } func updateNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: "Details", dropLast: 1 ), ] } func setupUpdateRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.get("update", use: updateView) existingModelRoutes.post("update", use: updateAction) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDashboardContext.swift ================================================ struct AdminDashboardContext { let icon: String let title: String let message: String } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDeletePageContext.swift ================================================ public struct AdminDeletePageContext { public let title: String public let name: String public let type: String public let form: FormContext public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public init( title: String, name: String, type: String, form: FormContext, navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [] ) { self.title = title self.name = name self.type = type self.form = form self.navigation = navigation self.breadcrumbs = breadcrumbs } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDetailPageContext.swift ================================================ public struct AdminDetailPageContext { public let title: String public let fields: [DetailContext] public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public let actions: [LinkContext] public init( title: String, fields: [DetailContext], navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [], actions: [LinkContext] = [] ) { self.title = title self.fields = fields self.navigation = navigation self.breadcrumbs = breadcrumbs self.actions = actions } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminEditorPageContext.swift ================================================ public struct AdminEditorPageContext { public let title: String public let form: FormContext public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public let actions: [LinkContext] public init( title: String, form: FormContext, navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [], actions: [LinkContext] = [] ) { self.title = title self.form = form self.navigation = navigation self.breadcrumbs = breadcrumbs self.actions = actions } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminIndexContext.swift ================================================ public struct AdminIndexContext { public let title: String public let breadcrumbs: [LinkContext] public init( title: String, breadcrumbs: [LinkContext] = [] ) { self.title = title self.breadcrumbs = breadcrumbs } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminListPageContext.swift ================================================ public struct AdminListPageContext { public let title: String public let table: TableContext public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public init( title: String, table: TableContext, navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [] ) { self.title = title self.table = table self.navigation = navigation self.breadcrumbs = breadcrumbs } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDashboardTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminDashboardTemplate: TemplateRepresentable { var context: AdminDashboardContext init( _ context: AdminDashboardContext ) { self.context = context } @TagBuilder func render(_ req: Request) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } Nav { H2("Blog") Ul { Li { A("Posts") .href("/admin/blog/posts/") } Li { A("Categories") .href("/admin/blog/categories/") } } } } .id("dashboard") .class("container") } .render(req) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDeletePageTemplate.swift ================================================ import Vapor import SwiftHtml public struct AdminDeletePageTemplate: TemplateRepresentable { var context: AdminDeletePageContext public init( _ context: AdminDeletePageContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { Span("🗑") .class("icon") H1(context.title) P("You are about to permanently delete the
`\(context.name)` \(context.type).") FormTemplate(context.form).render(req) A("Cancel") .href((try? req.query.get(String.self, at: "cancel")) ?? "#") .class(["button", "cancel"]) } .class(["lead", "container", "center"]) } .render(req) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDetailPageTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminDetailPageTemplate: TemplateRepresentable { var context: AdminDetailPageContext init( _ context: AdminDetailPageContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { Div { H1(context.title) for item in context.navigation { LinkTemplate(item).render(req) } } .class("lead") Dl { for item in context.fields { DetailTemplate(item).render(req) } } Section { for item in context.actions { LinkTemplate(item).render(req) } } } .class("container") } .render(req) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Admin/Templates/Html/AdminEditorPageTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminEditorPageTemplate: TemplateRepresentable { var context: AdminEditorPageContext init( _ context: AdminEditorPageContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { Div { H1(context.title) for item in context.navigation { LinkTemplate(item).render(req) } } .class("lead") FormTemplate(context.form).render(req) Section { for item in context.actions { LinkTemplate(item).render(req) } } } .class("container") } .render(req) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Admin/Templates/Html/AdminIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg public struct AdminIndexTemplate: TemplateRepresentable { public var context: AdminIndexContext var body: Tag public init( _ context: AdminIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Meta() .name("robots") .content("noindex") Link(rel: .shortcutIcon) .href("/images/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css") Link(rel: .stylesheet) .href("/css/admin.css") Title(context.title) } Body { Div { A { Img(src: "/img/logo.png", alt: "Logo") .title("Logo") .style("width: 300px") } .href("/") Nav { Input() .type(.checkbox) .id("secondary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("secondary-menu-button") Div { A("Sign out") .href("/sign-out/") } .class("menu-items") } .id("secondary-menu") } .id("navigation") Div { Nav { A("Admin") .href("/admin/") for breadcrumb in context.breadcrumbs { LinkTemplate(breadcrumb).render(req) } } } .class("breadcrumb") Main { body } Script() .type(.javascript) .src("/js/admin.js") } } .lang("en-US") } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Admin/Templates/Html/AdminListPageTemplate.swift ================================================ import Vapor import SwiftHtml public struct AdminListPageTemplate: TemplateRepresentable { var context: AdminListPageContext public init( _ context: AdminListPageContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { H1(context.title) P { context.navigation.map { LinkTemplate($0).render(req) } } } .class("lead") if context.table.rows.isEmpty { Div { Span("🔍") .class("icon") H2("Oh no") P("This list is empty right now.") A("Try again →") .href(req.url.path) .class("button-1") } .class(["lead", "container", "center"]) } else { TableTemplate(context.table).render(req) } } .render(req) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Api/ApiModule.swift ================================================ import Vapor struct ApiModule: ModuleInterface { func boot(_ app: Application) throws { app.middleware.use(ApiErrorMiddleware()) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Api/Controllers/ApiController.swift ================================================ import Vapor protocol ApiController: ApiListController, ApiDetailController, ApiCreateController, ApiUpdateController, ApiPatchController, ApiDeleteController { func validators( optional: Bool ) -> [AsyncValidator] func setupRoutes( _ routes: RoutesBuilder ) } extension ApiController { func validators( optional: Bool ) -> [AsyncValidator] { [] } func createValidators() -> [AsyncValidator] { validators(optional: false) } func updateValidators() -> [AsyncValidator] { validators(optional: false) } func patchValidators() -> [AsyncValidator] { validators(optional: true) } func createResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response { try await detailOutput(req, model) .encodeResponse(status: .created, for: req) } func updateResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response { try await detailOutput(req, model) .encodeResponse(for: req) } func patchResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response { try await detailOutput(req, model) .encodeResponse(for: req) } func setupRoutes( _ routes: RoutesBuilder ) { setupListRoutes(routes) setupDetailRoutes(routes) setupCreateRoutes(routes) setupUpdateRoutes(routes) setupPatchRoutes(routes) setupDeleteRoutes(routes) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Api/Controllers/ApiCreateController.swift ================================================ import Vapor protocol ApiCreateController: CreateController { associatedtype CreateObject: Decodable func createValidators() -> [AsyncValidator] func createInput( _ req: Request, _ model: DatabaseModel, _ input: CreateObject ) async throws func createApi( _ req: Request ) async throws -> Response func createResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response func setupCreateRoutes( _ routes: RoutesBuilder ) } extension ApiCreateController { func createValidators() -> [AsyncValidator] { [] } func createApi( _ req: Request ) async throws -> Response { try await RequestValidator(createValidators()).validate(req) let input = try req.content.decode(CreateObject.self) let model = DatabaseModel() try await createInput(req, model, input) try await create(req, model) return try await createResponse(req, model) } func setupCreateRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) baseRoutes.post(use: createApi) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Api/Controllers/ApiDeleteController.swift ================================================ import Vapor public protocol ApiDeleteController: DeleteController { func deleteApi( _ req: Request ) async throws -> HTTPStatus func setupDeleteRoutes( _ routes: RoutesBuilder ) } public extension ApiDeleteController { func deleteApi( _ req: Request ) async throws -> HTTPStatus { let model = try await findBy(identifier(req), on: req.db) try await delete(req, model) return .noContent } func setupDeleteRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.delete(use: deleteApi) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Api/Controllers/ApiDetailController.swift ================================================ import Vapor protocol ApiDetailController: DetailController { associatedtype DetailObject: Content func detailOutput( _ req: Request, _ model: DatabaseModel ) async throws -> DetailObject func detailApi( _ req: Request ) async throws -> DetailObject func setupDetailRoutes( _ routes: RoutesBuilder ) } extension ApiDetailController { func detailApi( _ req: Request ) async throws -> DetailObject { let model = try await detail(req) return try await detailOutput(req, model) } func setupDetailRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.get(use: detailApi) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Api/Controllers/ApiListController.swift ================================================ import Vapor protocol ApiListController: ListController { associatedtype ListObject: Content func listOutput( _ req: Request, _ models: [DatabaseModel] ) async throws -> [ListObject] func listApi( _ req: Request ) async throws -> [ListObject] func setupListRoutes( _ routes: RoutesBuilder ) } extension ApiListController { func listApi( _ req: Request ) async throws -> [ListObject] { let models = try await list(req) return try await listOutput(req, models) } func setupListRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) baseRoutes.get(use: listApi) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Api/Controllers/ApiPatchController.swift ================================================ import Vapor public protocol ApiPatchController: PatchController { associatedtype PatchObject: Decodable func patchValidators() -> [AsyncValidator] func patchInput( _ req: Request, _ model: DatabaseModel, _ input: PatchObject ) async throws func patchApi( _ req: Request ) async throws -> Response func patchResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response func setupPatchRoutes( _ routes: RoutesBuilder ) } public extension ApiPatchController { func patchValidators() -> [AsyncValidator] { [] } func patchApi( _ req: Request ) async throws -> Response { try await RequestValidator(patchValidators()).validate(req) let model = try await findBy(identifier(req), on: req.db) let input = try req.content.decode(PatchObject.self) try await patchInput(req, model, input) try await patch(req, model) return try await patchResponse(req, model) } func setupPatchRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.patch(use: patchApi) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Api/Controllers/ApiUpdateController.swift ================================================ import Vapor public protocol ApiUpdateController: UpdateController { associatedtype UpdateObject: Decodable func updateValidators() -> [AsyncValidator] func updateInput( _ req: Request, _ model: DatabaseModel, _ input: UpdateObject ) async throws func updateApi( _ req: Request ) async throws -> Response func updateResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response func setupUpdateRoutes( _ routes: RoutesBuilder ) } public extension ApiUpdateController { func updateValidators() -> [AsyncValidator] { [] } func updateApi( _ req: Request ) async throws -> Response { try await RequestValidator(updateValidators()).validate(req) let model = try await findBy(identifier(req), on: req.db) let input = try req.content.decode(UpdateObject.self) try await updateInput(req, model, input) try await update(req, model) return try await updateResponse(req, model) } func setupUpdateRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.put(use: updateApi) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Api/Middlewares/ApiErrorMiddleware.swift ================================================ import Vapor struct ApiErrorMiddleware: AsyncMiddleware { func respond( to req: Request, chainingTo next: AsyncResponder ) async throws -> Response { do { return try await next.respond(to: req) } catch { let status: HTTPResponseStatus let headers: HTTPHeaders let message: String? let details: [ValidationErrorDetail] switch error { case let abort as ValidationAbort: status = abort.abort.status headers = abort.abort.headers message = abort.message details = abort.details case let abort as Abort: status = abort.status headers = abort.headers message = abort.reason details = [] default: status = .internalServerError headers = [:] if req.application.environment.isRelease { message = "Something went wrong." } else { message = error.localizedDescription } details = [] } req.logger.report(error: error) let response = Response( status: status, headers: headers ) do { let encoder = JSONEncoder() let data = try encoder.encode( ValidationError( message: message, details: details ) ) response.body = .init(data: data) response.headers.replaceOrAdd( name: .contentType, value: "application/json; charset=utf-8" ) } catch { response.body = .init(string: "Oops: \(error)") response.headers.replaceOrAdd( name: .contentType, value: "text/plain; charset=utf-8" ) } return response } } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/BlogModule.swift ================================================ import Vapor struct BlogModule: ModuleInterface { let router = BlogRouter() func boot(_ app: Application) throws { app.migrations.add(BlogMigrations.v1()) app.migrations.add(BlogMigrations.seed()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/BlogRouter.swift ================================================ import Vapor struct BlogRouter: RouteCollection { let frontendController = BlogFrontendController() let postAdminController = BlogPostAdminController() let postApiController = BlogPostApiController() let categoryAdminController = BlogCategoryAdminController() let categoryApiController = BlogCategoryApiController() func boot(routes: RoutesBuilder) throws { routes.get("blog", use: frontendController.blogView) routes.get(.anything, use: frontendController.postView) let admin = routes .grouped(AuthenticatedUser.redirectMiddleware(path: "/")) .grouped("admin") postAdminController.setupRoutes(admin) categoryAdminController.setupRoutes(admin) let api = routes .grouped(AuthenticatedUser.guardMiddleware()) .grouped("api") postApiController.setupRoutes(api) categoryApiController.setupRoutes(api) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/Controllers/BlogCategoryAdminController.swift ================================================ import Vapor import Fluent struct BlogCategoryAdminController: AdminController { typealias ApiModel = Blog.Category typealias DatabaseModel = BlogCategoryModel typealias CreateModelEditor = BlogCategoryEditor typealias UpdateModelEditor = BlogCategoryEditor func listColumns() -> [ColumnContext] { [ .init("title"), ] } func listCells( for model: DatabaseModel ) -> [CellContext] { [ .init( model.title, link: .init(label: model.title) ), ] } func detailFields( for model: DatabaseModel ) -> [DetailContext] { [ .init("title", model.title), ] } func deleteInfo( _ model: DatabaseModel ) -> String { model.title } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/Controllers/BlogCategoryApiController.swift ================================================ import Vapor extension Blog.Category.List: Content {} extension Blog.Category.Detail: Content {} struct BlogCategoryApiController: ApiController { typealias ApiModel = Blog.Category typealias DatabaseModel = BlogCategoryModel @AsyncValidatorBuilder func validators( optional: Bool ) -> [AsyncValidator] { KeyedContentValidator.required("title", optional: optional) } func listOutput( _ req: Request, _ models: [BlogCategoryModel] ) async throws -> [Blog.Category.List] { models.map { .init(id: $0.id!, title: $0.title) } } func detailOutput( _ req: Request, _ model: BlogCategoryModel ) async throws -> Blog.Category.Detail { .init( id: model.id!, title: model.title ) } func createInput( _ req: Request, _ model: BlogCategoryModel, _ input: Blog.Category.Create ) async throws { model.title = input.title } func updateInput( _ req: Request, _ model: BlogCategoryModel, _ input: Blog.Category.Update ) async throws { model.title = input.title } func patchInput( _ req: Request, _ model: BlogCategoryModel, _ input: Blog.Category.Patch ) async throws { model.title = input.title ?? model.title } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift ================================================ import Vapor import Fluent struct BlogFrontendController { func blogView(req: Request) async throws -> Response { let posts = try await BlogPostModel .query(on: req.db) .sort(\.$date, .descending) .all() let api = BlogPostApiController() let listOutput = try await api.listOutput(req, posts) let ctx = BlogPostsContext( icon: "🔥", title: "Blog", message: "Hot news and stories about everything.", posts: listOutput ) return req.templates.renderHtml(BlogPostsTemplate(ctx)) } func postView(req: Request) async throws -> Response { let slug = req.url.path.trimmingCharacters( in: .init(charactersIn: "/") ) guard let post = try await BlogPostModel .query(on: req.db) .filter(\.$slug == slug) .first() else { return req.redirect(to: "/") } let api = BlogPostApiController() let postOutput = try await api.detailOutput(req, post) let ctx = BlogPostContext(post: postOutput) return req.templates.renderHtml(BlogPostTemplate(ctx)) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/Controllers/BlogPostAdminController.swift ================================================ import Vapor import Fluent struct BlogPostAdminController: AdminController { typealias ApiModel = Blog.Post typealias DatabaseModel = BlogPostModel typealias CreateModelEditor = BlogPostEditor typealias UpdateModelEditor = BlogPostEditor func listColumns() -> [ColumnContext] { [ .init("image"), .init("title"), ] } func listCells(for model: DatabaseModel) -> [CellContext] { [ .init(model.imageKey, type: .image), .init(model.title, link: .init(label: model.title)), ] } func detailFields(for model: DatabaseModel) -> [DetailContext] { [ .init("image", model.imageKey, type: .image), .init("title", model.title), ] } func deleteInfo( _ model: DatabaseModel ) -> String { model.title } func beforeDelete( _ req: Request, _ model: BlogPostModel ) async throws { try await req.fs.delete(key: model.imageKey) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/Controllers/BlogPostApiController.swift ================================================ import Vapor extension Blog.Post.List: Content {} extension Blog.Post.Detail: Content {} struct BlogPostApiController: ApiController { typealias ApiModel = Blog.Post typealias DatabaseModel = BlogPostModel @AsyncValidatorBuilder func validators(optional: Bool) -> [AsyncValidator] { KeyedContentValidator.required("title", optional: optional) KeyedContentValidator.required("slug", optional: optional) KeyedContentValidator.required("image", optional: optional) KeyedContentValidator.required("excerpt", optional: optional) KeyedContentValidator.required("content", optional: optional) KeyedContentValidator.required("categoryId", optional: optional) KeyedContentValidator("categoryId", "Invalid or missing category", optional: optional) { value, req in try await BlogCategoryModel.find(value, on: req.db) != nil } } func listOutput( _ req: Request, _ models: [DatabaseModel] ) async throws -> [Blog.Post.List] { models.map { model in .init( id: model.id!, title: model.title, slug: model.slug, image: model.imageKey, excerpt: model.excerpt, date: model.date ) } } func detailOutput( _ req: Request, _ model: DatabaseModel ) async throws -> Blog.Post.Detail { guard let categoryModel = try await BlogCategoryModel .find(model.$category.id, on: req.db), let category = try await BlogCategoryApiController() .listOutput(req, [categoryModel]) .first else { throw Abort(.internalServerError) } return .init( id: model.id!, title: model.title, slug: model.slug, image: model.imageKey, excerpt: model.excerpt, date: model.date, category: category, content: model.content ) } func createInput( _ req: Request, _ model: DatabaseModel, _ input: Blog.Post.Create ) async throws { model.title = input.title model.slug = input.slug model.imageKey = input.image model.excerpt = input.excerpt model.date = input.date model.content = input.content model.$category.id = input.categoryId } func updateInput( _ req: Request, _ model: DatabaseModel, _ input: Blog.Post.Update ) async throws { model.title = input.title model.slug = input.slug model.imageKey = input.image model.excerpt = input.excerpt model.date = input.date model.content = input.content model.$category.id = input.categoryId } func patchInput( _ req: Request, _ model: DatabaseModel, _ input: Blog.Post.Patch ) async throws { model.title = input.title ?? model.title model.slug = input.slug ?? model.slug model.imageKey = input.image ?? model.imageKey model.excerpt = input.excerpt ?? model.excerpt model.date = input.date ?? model.date model.content = input.content ?? model.content model.$category.id = input.categoryId ?? model.$category.id } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/Database/Migrations/BlogMigrations.swift ================================================ import Foundation import Fluent enum BlogMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema) .id() .field(BlogCategoryModel.FieldKeys.v1.title, .string, .required) .create() try await db.schema(BlogPostModel.schema) .id() .field(BlogPostModel.FieldKeys.v1.title, .string, .required) .field(BlogPostModel.FieldKeys.v1.slug, .string, .required) .field(BlogPostModel.FieldKeys.v1.imageKey, .string, .required) .field(BlogPostModel.FieldKeys.v1.excerpt, .data, .required) .field(BlogPostModel.FieldKeys.v1.date, .datetime, .required) .field(BlogPostModel.FieldKeys.v1.content, .data, .required) .field(BlogPostModel.FieldKeys.v1.categoryId, .uuid) .foreignKey( BlogPostModel.FieldKeys.v1.categoryId, references: BlogCategoryModel.schema, .id, onDelete: DatabaseSchema.ForeignKeyAction.setNull, onUpdate: .cascade ) .unique(on: BlogPostModel.FieldKeys.v1.slug) .create() } func revert(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema).delete() try await db.schema(BlogPostModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let categories = (1...4).map { index in BlogCategoryModel(title: "Sample category #\(index)") } try await categories.create(on: db) try await (1...9).map { index in BlogPostModel( id: nil, title: "Sample post #\(index)", slug: "sample-post-\(index)", imageKey: "/img/posts/\(String(format: "%02d", index + 1)).jpg", excerpt: "Lorem ipsum", date: Date().addingTimeInterval(-Double.random(in: 0...(86400 * 60))), content: "Lorem ipsum dolor sit amet.", categoryId: categories[Int.random(in: 0.. [FormComponent] { ImageField("image", path: "blog/post") .read { [unowned self] in $1.output.context.previewUrl = model.imageKey ($1 as! ImageField).imageKey = model.imageKey } .write { [unowned self] in model.imageKey = ($1 as! ImageField).imageKey ?? "" } InputField("slug") .config { $0.output.context.label.required = true } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = model.slug } .write { [unowned self] in model.slug = $1.input } InputField("title") .config { $0.output.context.label.required = true } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = model.title } .write { [unowned self] in model.title = $1.input } InputField("date") .config { $0.output.context.label.required = true $0.output.context.value = dateFormatter.string(from: Date()) } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = dateFormatter.string(from: model.date) } .write { [unowned self] in model.date = dateFormatter.date(from: $1.input) ?? Date() } TextareaField("excerpt") .read { [unowned self] in $1.output.context.value = model.excerpt } .write { [unowned self] in model.excerpt = $1.input } TextareaField("content") .read { [unowned self] in $1.output.context.value = model.content } .write { [unowned self] in model.content = $1.input } SelectField("category") .load { req, field in let categories = try await BlogCategoryModel .query(on: req.db) .all() field.output.context.options = categories.map { OptionContext(key: $0.id!.uuidString, label: $0.title) } } .read { [unowned self] req, field in field.output.context.value = model.$category.id.uuidString } .write { [unowned self] req, field in if let uuid = UUID(uuidString: field.input), let category = try await BlogCategoryModel .find(uuid, on: req.db) { model.$category.id = category.id! } } } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/Objects/Blog.swift ================================================ enum Blog: ApiModuleInterface { enum Post: ApiModelInterface { typealias Module = Blog } enum Category: ApiModelInterface { typealias Module = Blog static let pathKey: String = "categories" } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/Objects/BlogCategory.swift ================================================ import Foundation extension Blog.Category { struct List: Codable { let id: UUID let title: String } struct Detail: Codable { let id: UUID let title: String } struct Create: Codable { let title: String } struct Update: Codable { let title: String } struct Patch: Codable { let title: String? } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/Objects/BlogPost.swift ================================================ import Foundation extension Blog.Post { struct List: Codable { let id: UUID let title: String let slug: String let image: String let excerpt: String let date: Date } struct Detail: Codable { let id: UUID let title: String let slug: String let image: String let excerpt: String let date: Date let category: Blog.Category.List let content: String } struct Create: Codable { let title: String let slug: String let image: String let excerpt: String let date: Date let content: String let categoryId: UUID } struct Update: Codable { let title: String let slug: String let image: String let excerpt: String let date: Date let content: String let categoryId: UUID } struct Patch: Codable { let title: String? let slug: String? let image: String? let excerpt: String? let date: Date? let content: String? let categoryId: UUID? } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminDeleteContext.swift ================================================ struct BlogPostAdminDeleteContext { let title: String let name: String let type: String } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminDetailContext.swift ================================================ struct BlogPostAdminDetailContext { let title: String let detail: Blog.Post.Detail } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminEditContext.swift ================================================ struct BlogPostAdminEditContext { let title: String let form: TemplateRepresentable } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminListContext.swift ================================================ struct BlogPostAdminListContext { let title: String let list: [Blog.Post.List] } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostContext.swift ================================================ struct BlogPostContext { let post: Blog.Post.Detail } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostsContext.swift ================================================ struct BlogPostsContext { let icon: String let title: String let message: String let posts: [Blog.Post.List] } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminDeleteTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminDeleteTemplate: TemplateRepresentable { var context: BlogPostAdminDeleteContext init( _ context: BlogPostAdminDeleteContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Span("🗑") .class("icon") H1(context.title) P("You are about to permanently delete the
`\(context.name)` \(context.type).") Form { Input() .type(.submit) .class(["button", "destructive"]) .style("display: inline") .value("Delete") A("Cancel") .href("/admin/blog/posts/") .class(["button", "cancel"]) } .method(.post) .id("delete-form") } .class(["lead", "container", "center"]) } .render(req) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminDetailTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminDetailTemplate: TemplateRepresentable { var context: BlogPostAdminDetailContext init( _ context: BlogPostAdminDetailContext ) { self.context = context } var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .short return formatter }() @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") Dl { Dt("Image") Dd { Img( src: context.detail.image, alt: context.detail.title ) } Dt("Title") Dd(context.detail.title) Dt("Excerpt") Dd(context.detail.excerpt) Dt("Date") Dd(dateFormatter.string(from: context.detail.date)) Dt("Content") Dd(context.detail.content) } } .id("detail") .class("container") } .render(req) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminEditTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminEditTemplate: TemplateRepresentable { var context: BlogPostAdminEditContext init( _ context: BlogPostAdminEditContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") context.form.render(req) } .id("edit") .class("container") } .render(req) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminListTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminListTemplate: TemplateRepresentable { var context: BlogPostAdminListContext init( _ context: BlogPostAdminListContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") Table { Thead { Tr { Th("Image") Th("Title") Th("Preview") } } Tbody { for item in context.list { Tr { Td { Img(src: item.image, alt: item.title) } Td { A(item.title) .href("/admin/blog/posts/" + item.id.uuidString + "/") } Td { A("Preview") .href("/" + item.slug + "/") } } } } } } .id("list") } .render(req) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostTemplate: TemplateRepresentable { var context: BlogPostContext init( _ context: BlogPostContext ) { self.context = context } var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .short return formatter }() @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.post.title) ) { Div { Section { P(dateFormatter.string(from: context.post.date)) H1(context.post.title) P(context.post.excerpt) } .class(["lead", "container"]) Img(src: context.post.image, alt: context.post.title) Article { Text(context.post.content) } .class("container") } .id("post") } .render(req) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostsTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostsTemplate: TemplateRepresentable { var context: BlogPostsContext init( _ context: BlogPostsContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") Div { for post in context.posts { Article { A { Img(src: post.image, alt: post.title) H2(post.title) P(post.excerpt) } .href("/\(post.slug)/") } } } .class("grid-221") } .id("blog") } .render(req) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/User/Authenticators/UserCredentialsAuthenticator.swift ================================================ import Vapor import Fluent struct UserCredentialsAuthenticator: AsyncCredentialsAuthenticator { struct Credentials: Content { let email: String let password: String } func authenticate( credentials: Credentials, for req: Request ) async throws { guard let user = try await UserAccountModel .query(on: req.db) .filter(\.$email == credentials.email) .first() else { return } do { guard try Bcrypt.verify( credentials.password, created: user.password ) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } catch { // do nothing... } } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/User/Authenticators/UserSessionAuthenticator.swift ================================================ import Vapor import Fluent struct UserSessionAuthenticator: AsyncSessionAuthenticator { typealias User = AuthenticatedUser func authenticate( sessionID: User.SessionID, for req: Request ) async throws { guard let user = try await UserAccountModel .find(sessionID, on: req.db) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/User/Authenticators/UserTokenAuthenticator.swift ================================================ import Vapor import Fluent struct UserTokenAuthenticator: AsyncBearerAuthenticator { func authenticate( bearer: BearerAuthorization, for req: Request ) async throws { guard let token = try await UserTokenModel .query(on: req.db) .filter(\.$value == bearer.token) .first() else { return } guard let user = try await UserAccountModel .find(token.$user.id, on: req.db) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/User/Controllers/UserApiController.swift ================================================ import Vapor extension User.Token.Detail: Content {} struct UserApiController { func signInApi( req: Request ) async throws -> User.Token.Detail { guard let user = req.auth.get(AuthenticatedUser.self) else { throw Abort(.unauthorized) } let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789=" let tokenValue = String((0..<64).map { _ in letters.randomElement()! }) let token = UserTokenModel( value: tokenValue, userId: user.id ) try await token.create(on: req.db) let account = User.Account.Detail( id: user.id, email: user.email ) return .init( id: token.id!, value: token.value, user: account ) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/User/Controllers/UserFrontendController.swift ================================================ import Vapor struct UserFrontendController { private func renderSignInView( _ req: Request, _ form: UserLoginForm ) -> Response { let template = UserLoginTemplate( .init( icon: "⬇️", title: "Sign in", message: "Please log in with your existing account", form: form.render(req: req) ) ) return req.templates.renderHtml(template) } func signInView( _ req: Request ) async throws -> Response { renderSignInView(req, .init()) } func signInAction( _ req: Request ) async throws -> Response { /// the user is authenticated, we can store the user data inside the session too if let user = req.auth.get(AuthenticatedUser.self) { req.session.authenticate(user) return req.redirect(to: "/") } let form = UserLoginForm() try await form.process(req: req) let isValid = try await form.validate(req: req) if !isValid { form.error = "Invalid email or password." } return renderSignInView(req, form) } func signOut( _ req: Request ) throws -> Response { req.auth.logout(AuthenticatedUser.self) req.session.unauthenticate(AuthenticatedUser.self) return req.redirect(to: "/") } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/User/Database/Migrations/UserMigrations.swift ================================================ import Vapor import Fluent enum UserMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(UserAccountModel.schema) .id() .field(UserAccountModel.FieldKeys.v1.email, .string, .required) .field(UserAccountModel.FieldKeys.v1.password, .string, .required) .unique(on: UserAccountModel.FieldKeys.v1.email) .create() try await db.schema(UserTokenModel.schema) .id() .field(UserTokenModel.FieldKeys.v1.value, .string, .required) .field(UserTokenModel.FieldKeys.v1.userId, .uuid, .required) .foreignKey( UserTokenModel.FieldKeys.v1.userId, references: UserAccountModel.schema, .id ) .unique(on: UserTokenModel.FieldKeys.v1.value) .create() } func revert(on db: Database) async throws { try await db.schema(UserTokenModel.schema).delete() try await db.schema(UserAccountModel.schema).delete() } } // ... struct seed: AsyncMigration { func prepare(on db: Database) async throws { let email = "root@localhost.com" let password = "ChangeMe1" let user = UserAccountModel( email: email, password: try Bcrypt.hash(password) ) try await user.create(on: db) } func revert(on db: Database) async throws { try await UserAccountModel.query(on: db).delete() } } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/User/Database/Models/UserAccountModel.swift ================================================ import Vapor import Fluent final class UserAccountModel: DatabaseModelInterface { typealias Module = UserModule struct FieldKeys { struct v1 { static var email: FieldKey { "email" } static var password: FieldKey { "password" } } } @ID() var id: UUID? @Field(key: FieldKeys.v1.email) var email: String @Field(key: FieldKeys.v1.password) var password: String init() { } init( id: UUID? = nil, email: String, password: String ) { self.id = id self.email = email self.password = password } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/User/Database/Models/UserTokenModel.swift ================================================ import Vapor import Fluent final class UserTokenModel: DatabaseModelInterface { typealias Module = UserModule struct FieldKeys { struct v1 { static var value: FieldKey { "value" } static var userId: FieldKey { "user_id" } } } @ID() var id: UUID? @Field(key: FieldKeys.v1.value) var value: String @Parent(key: FieldKeys.v1.userId) var user: UserAccountModel init() { } init( id: UUID? = nil, value: String, userId: UUID ) { self.id = id self.value = value self.$user.id = userId } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/User/Forms/UserLoginForm.swift ================================================ import Vapor final class UserLoginForm: AbstractForm { public convenience init() { self.init( action: .init( method: .post, url: "/sign-in/" ), submit: "Sign in" ) self.fields = createFields() } @FormComponentBuilder func createFields() -> [FormComponent] { InputField("email") .config { $0.output.context.label.required = true $0.output.context.type = .email } .validators { FormFieldValidator.required($1) FormFieldValidator.email($1) } InputField("password") .config { $0.output.context.label.required = true $0.output.context.type = .password } .validators { FormFieldValidator.required($1) } } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/User/Objects/User.swift ================================================ enum User: ApiModuleInterface { enum Account: ApiModelInterface { typealias Module = User } enum Token: ApiModelInterface { typealias Module = User } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/User/Objects/UserAccount.swift ================================================ import Foundation extension User.Account { struct List: Codable { let id: UUID let email: String } struct Detail: Codable { let id: UUID let email: String } struct Create: Codable { let email: String let password: String } struct Update: Codable { let email: String let password: String? } struct Patch: Codable { let email: String? let password: String? } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/User/Objects/UserToken.swift ================================================ import Foundation extension User.Token { struct Detail: Codable { let id: UUID let value: String let user: User.Account.Detail } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/User/Templates/Contexts/UserLoginContext.swift ================================================ struct UserLoginContext { let icon: String let title: String let message: String let form: TemplateRepresentable init( icon: String, title: String, message: String, form: TemplateRepresentable ) { self.icon = icon self.title = title self.message = message self.form = form } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/User/Templates/Html/UserLoginTemplate.swift ================================================ import Vapor import SwiftHtml struct UserLoginTemplate: TemplateRepresentable { var context: UserLoginContext init( _ context: UserLoginContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") context.form.render(req) } .id("user-login") .class("container") } .render(req) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/User/UserModule.swift ================================================ import Vapor struct UserModule: ModuleInterface { let router = UserRouter() func boot(_ app: Application) throws { app.migrations.add(UserMigrations.v1()) app.migrations.add(UserMigrations.seed()) app.middleware.use(UserSessionAuthenticator()) app.middleware.use(UserTokenAuthenticator()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/User/UserRouter.swift ================================================ import Vapor struct UserRouter: RouteCollection { let frontendController = UserFrontendController() let apiController = UserApiController() func boot(routes: RoutesBuilder) throws { routes.get("sign-in", use: frontendController.signInView) routes .grouped(UserCredentialsAuthenticator()) .post("sign-in", use: frontendController.signInAction) routes.get("sign-out", use: frontendController.signOut) routes.grouped("api") .grouped(UserCredentialsAuthenticator()) .post("sign-in", use: apiController.signInApi) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Web/Controllers/WebFrontendController.swift ================================================ import Vapor struct WebFrontendController { func homeView(req: Request) throws -> Response { let ctx = WebHomeContext( icon: "👋", title: "Home", message: "Hi there, welcome to my page.", paragraphs: [ "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.", "Nisi ut aliquip ex ea commodo consequat.", ], link: .init( label: "Read my blog →", url: "/blog/" ) ) return req.templates.renderHtml( WebHomeTemplate(ctx) ) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Web/Templates/Contexts/WebHomeContext.swift ================================================ struct WebHomeContext { let icon: String let title: String let message: String let paragraphs: [String] let link: WebLinkContext } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Web/Templates/Contexts/WebIndexContext.swift ================================================ public struct WebIndexContext { public let title: String public init( title: String ) { self.title = title } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Web/Templates/Contexts/WebLinkContext.swift ================================================ public struct WebLinkContext { public let label: String public let url: String public init( label: String, url: String ) { self.label = label self.url = url } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Web/Templates/Html/WebHomeTemplate.swift ================================================ import Vapor import SwiftHtml struct WebHomeTemplate: TemplateRepresentable { var context: WebHomeContext init( _ context: WebHomeContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") for paragraph in context.paragraphs { P(paragraph) } WebLinkTemplate(context.link).render(req) } .id("home") .class("container") } .render(req) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg public struct WebIndexTemplate: TemplateRepresentable { public var context: WebIndexContext var body: Tag public init( _ context: WebIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Link(rel: .shortcutIcon) .href("/images/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css") Link(rel: .stylesheet) .href("/css/web.css") Title(context.title) } Body { Header { Div { A { Img(src: "/img/logo.png", alt: "Logo") } .id("site-logo") .href("/") Nav { Input() .type(.checkbox) .id("primary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("primary-menu-button") Div { A("Home") .href("/") .class("selected", req.url.path == "/") A("Blog") .href("/blog/") .class("selected", req.url.path == "/blog/") A("About") .href("#") .onClick("javascript:about();") if req.auth.has(AuthenticatedUser.self) { A("Admin") .href("/admin/") A("Sign out") .href("/sign-out/") } else { A("Sign in") .href("/sign-in/") } } .class("menu-items") } .id("primary-menu") } .id("navigation") } Main { body } Footer { Section { P { Text("This site is powered by ") A("Swift") .href("https://swift.org") .target(.blank) Text(" & ") A("Vapor") .href("https://vapor.codes") .target(.blank) Text(".") } P("myPage © 2020-2022") } } Script() .type(.javascript) .src("/js/web.js") } } .lang("en-US") } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Web/Templates/Html/WebLinkTemplate.swift ================================================ import Vapor import SwiftHtml struct WebLinkTemplate: TemplateRepresentable { var context: WebLinkContext init( _ context: WebLinkContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { A(context.label) .href(context.url) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Web/WebModule.swift ================================================ import Vapor struct WebModule: ModuleInterface { let router = WebRouter() func boot(_ app: Application) throws { try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Modules/Web/WebRouter.swift ================================================ import Vapor struct WebRouter: RouteCollection { let frontendController = WebFrontendController() func boot( routes: RoutesBuilder ) throws { routes.get(use: frontendController.homeView) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Template/Request+Template.swift ================================================ import Vapor public extension Request { var templates: TemplateRenderer { .init(self) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Template/TemplateRenderer.swift ================================================ import Vapor import SwiftHtml public struct TemplateRenderer { var req: Request init(_ req: Request) { self.req = req } public func renderHtml( _ template: TemplateRepresentable, minify: Bool = false, indent: Int = 4 ) -> Response { let doc = Document(.html) { template.render(req) } let body = DocumentRenderer( minify: minify, indent: indent ) .render(doc) return Response( status: .ok, headers: [ "content-type": "text/html" ], body: .init(string: body) ) } } ================================================ FILE: Chapter 14/myProject/Sources/App/Template/TemplateRepresentable.swift ================================================ /// FILE: Sources/App/Template/TemplateRepresentable.swift import Vapor import SwiftSgml public protocol TemplateRepresentable { @TagBuilder func render(_ req: Request) -> Tag } ================================================ FILE: Chapter 14/myProject/Sources/App/configure.swift ================================================ import Vapor import Fluent import FluentSQLiteDriver import Liquid import LiquidLocalDriver public func configure( _ app: Application ) throws { app.fileStorages.use( .local( publicUrl: "http://localhost:8080", publicPath: app.directory.publicDirectory, workDirectory: "assets" ), as: .local ) app.routes.defaultMaxBodySize = "10mb" let dbPath = app.directory.resourcesDirectory + "db.sqlite" app.databases.use(.sqlite(.file(dbPath)), as: .sqlite) app.middleware.use( FileMiddleware( publicDirectory: app.directory.publicDirectory ) ) app.middleware.use(ExtendPathMiddleware()) app.sessions.use(.fluent) app.migrations.add(SessionRecord.migration) app.middleware.use(app.sessions.middleware) let modules: [ModuleInterface] = [ WebModule(), UserModule(), AdminModule(), ApiModule(), BlogModule(), ] for module in modules { try module.boot(app) } try app.autoMigrate().wait() } ================================================ FILE: Chapter 14/myProject/Sources/App/routes.swift ================================================ import Vapor import SwiftHtml func routes(_ app: Application) throws { app.routes.get { req -> Response in req.templates.renderHtml( WebIndexTemplate( WebIndexContext( title: "Home" ) ) { P("Hi there, welcome to my page!") } ) } } ================================================ FILE: Chapter 14/myProject/Sources/Run/main.swift ================================================ import App import Vapor var env = try Environment.detect() try LoggingSystem.bootstrap(from: &env) let app = Application(env) defer { app.shutdown() } try configure(app) try app.run() ================================================ FILE: Chapter 14/myProject/Tests/AppTests/AppTests.swift ================================================ @testable import App import XCTVapor final class AppTests: AppTestCase { func testHomePage() throws { let app = try createTestApp() defer { app.shutdown() } try app.testable(method: .inMemory).test(.GET, "") { res in XCTAssertEqual(res.status, .ok) let contentType = try XCTUnwrap(res.headers.contentType) XCTAssertEqual(contentType, .html) XCTAssertTrue(res.body.string.contains("Home")) } } func testAuth() throws { let app = try createTestApp() defer { app.shutdown() } let email = "root@localhost.com" let token = try authenticate( .init( email: email, password: "ChangeMe1" ), app ) XCTAssertEqual(token.user.email, email) } } ================================================ FILE: Chapter 14/myProject/Tests/AppTests/BlogCategoryApiTests.swift ================================================ @testable import App import XCTVapor extension Blog.Category.Create: Content {} final class BlogCategoryApiTests: AppTestCase { func testList() throws { let app = try createTestApp() let token = try authenticateRoot(app) defer { app.shutdown() } let headers = HTTPHeaders([("Authorization", "Bearer \(token.value)")]) try app //.testable(method: .inMemory) .testable(method: .running(port: 8081)) .test(.GET, "/api/blog/categories/", headers: headers) { res in XCTAssertEqual(res.status, .ok) let contentType = try XCTUnwrap(res.headers.contentType) XCTAssertEqual(contentType, .json) XCTAssertContent([Blog.Category.List].self, res) { content in XCTAssertEqual(content.count, 4) } } } func testCreate() throws { let app = try createTestApp() let token = try authenticateRoot(app) defer { app.shutdown() } let headers = HTTPHeaders([("Authorization", "Bearer \(token.value)")]) let newCategory = Blog.Category.Create(title: "Test category") try app.test( .POST, "/api/blog/categories/", headers: headers, content: newCategory ) { res in XCTAssertEqual(res.status, .created) let contentType = try XCTUnwrap(res.headers.contentType) XCTAssertEqual(contentType, .json) XCTAssertContent(Blog.Category.Detail.self, res) { content in XCTAssertEqual(content.title, newCategory.title) } } } func testCreateListUpdate() throws { let app = try createTestApp() let token = try authenticateRoot(app) defer { app.shutdown() } let headers = HTTPHeaders([("Authorization", "Bearer \(token.value)")]) let newCategory = Blog.Category.Create(title: "Test category") try app .test( .POST, "/api/blog/categories/", headers: headers, content: newCategory ) { res in XCTAssertEqual(res.status, .created) let contentType = try XCTUnwrap(res.headers.contentType) XCTAssertEqual(contentType, .json) XCTAssertContent(Blog.Category.Detail.self, res) { content in XCTAssertEqual(content.title, newCategory.title) } } .test( .GET, "/api/blog/categories/", headers: headers ) { res in XCTAssertEqual(res.status, .ok) let contentType = try XCTUnwrap(res.headers.contentType) XCTAssertEqual(contentType, .json) XCTAssertContent([Blog.Category.List].self, res) { content in XCTAssertEqual(content.count, 5) } } } } ================================================ FILE: Chapter 14/myProject/Tests/AppTests/BlogPostApiTests.swift ================================================ @testable import App import XCTVapor import Spec extension Blog.Post.Create: Content {} extension Blog.Post.Update: Content {} final class BlogPostApiTests: AppTestCase { func testList() throws { let app = try createTestApp() let token = try authenticateRoot(app) defer { app.shutdown() } try app .describe("Blog posts list API should be fine") .get("/api/blog/posts/") .bearerToken(token.value) .expect(.ok) .expect(.json) .expect([Blog.Post.List].self) { content in XCTAssertEqual(content.count, 9) } .test() } func testCreate() async throws { let app = try createTestApp() let token = try authenticateRoot(app) defer { app.shutdown() } let category = try await BlogCategoryModel.query(on: app.db).first() guard let category = category else { XCTFail("Missing default category") throw Abort(.notFound) } let newPost = Blog.Post.Create( title: "Dummy post", slug: "dummy-slug", image: "/dummy/image.jpg", excerpt: "Lorem ipsum", date: Date(), content: "Lorem ipsum", categoryId: category.id! ) try app .describe("Create post should be fine") .post("/api/blog/posts/") .body(newPost) .bearerToken(token.value) .expect(.created) .expect(.json) .expect(Blog.Post.Detail.self) { content in XCTAssertEqual(content.title, newPost.title) } .test() } func testUpdate() async throws { let app = try self.createTestApp() let token = try authenticateRoot(app) defer { app.shutdown() } guard let post = try await BlogPostModel .query(on: app.db) .with(\.$category) .first() else { XCTFail("Missing blog post") throw Abort(.notFound) } let suffix = " updated" let newPost = Blog.Post.Update( title: post.title + suffix, slug: post.slug + suffix, image: post.imageKey + suffix, excerpt: post.excerpt + suffix, date: post.date, content: post.content + suffix, categoryId: post.category.id! ) try app .describe("Update post should be fine") .put("/api/blog/posts/\(post.id!.uuidString)/") .body(newPost) .bearerToken(token.value) .expect(.ok) .expect(.json) .expect(Blog.Post.Detail.self) { content in XCTAssertEqual(content.id, post.id) XCTAssertEqual(content.title, newPost.title) XCTAssertEqual(content.slug, newPost.slug) XCTAssertEqual(content.image, newPost.image) XCTAssertEqual(content.excerpt, newPost.excerpt) XCTAssertEqual(content.content, newPost.content) } .test() } } ================================================ FILE: Chapter 14/myProject/Tests/AppTests/Framework/AppTestCase.swift ================================================ @testable import App import XCTVapor class AppTestCase: XCTestCase { struct UserLogin: Content { let email: String let password: String } func createTestApp() throws -> Application { let app = Application(.testing) try configure(app) app.databases.reinitialize() app.databases.use(.sqlite(.memory), as: .sqlite) app.databases.default(to: .sqlite) try app.autoMigrate().wait() return app } func authenticate( _ user: UserLogin, _ app: Application ) throws -> User.Token.Detail { var token: User.Token.Detail? try app.test(.POST, "/api/sign-in/", beforeRequest: { req in try req.content.encode(user) }, afterResponse: { res in XCTAssertContent(User.Token.Detail.self, res) { content in token = content } }) guard let result = token else { XCTFail("Login failed") throw Abort(.unauthorized) } return result } func authenticateRoot( _ app: Application ) throws -> User.Token.Detail { try authenticate( .init( email: "root@localhost.com", password: "ChangeMe1" ), app ) } } ================================================ FILE: Chapter 14/myProject/Tests/AppTests/Framework/XCTApplicationTester.swift ================================================ import XCTVapor extension XCTApplicationTester { @discardableResult public func test( _ method: HTTPMethod, _ path: String, headers: HTTPHeaders = [:], content: T, afterResponse: (XCTHTTPResponse) throws -> () = { _ in } ) throws -> XCTApplicationTester where T: Content { try test(method, path, headers: headers, beforeRequest: { req in try req.content.encode(content) }, afterResponse: afterResponse) } } ================================================ FILE: Chapter 14/myProject/docker-compose.yml ================================================ # 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 # Stop all: docker compose down # x-shared_environment: &shared_environment LOG_LEVEL: ${LOG_LEVEL:-debug} services: app: image: myproject:latest build: context: . environment: <<: *shared_environment 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"] ================================================ FILE: Chapter 15/myProject/Dockerfile ================================================ # ================================ # Build image # ================================ FROM swift:6.0-noble 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 \ $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true) # Copy entire repo into container COPY . . # Build the application, 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 \ --product myProject \ --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)/myProject" ./ # 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:noble # 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 ["./myProject"] CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] ================================================ FILE: Chapter 15/myProject/Package.swift ================================================ // swift-tools-version:5.7 import PackageDescription let package = Package( name: "myProject", platforms: [ .macOS(.v12) ], dependencies: [ .package( url: "https://github.com/vapor/vapor", from: "4.70.0" ), .package( url: "https://github.com/vapor/fluent", from: "4.4.0" ), .package( url: "https://github.com/vapor/fluent-sqlite-driver", from: "4.1.0" ), .package( url: "https://github.com/binarybirds/liquid", from: "1.3.0" ), .package( url: "https://github.com/binarybirds/liquid-local-driver", from: "1.3.0" ), .package( url: "https://github.com/binarybirds/swift-html", from: "1.7.0" ), .package( url: "https://github.com/binarybirds/spec", from: "1.2.0" ), ], targets: [ .target(name: "App", dependencies: [ .product(name: "Vapor", package: "vapor"), .product(name: "Fluent", package: "fluent"), .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), .product(name: "Liquid", package: "liquid"), .product(name: "LiquidLocalDriver", package: "liquid-local-driver"), .product(name: "SwiftHtml", package: "swift-html"), .product(name: "SwiftSvg", package: "swift-html"), ]), .executableTarget(name: "Run", dependencies: ["App"]), .testTarget(name: "AppTests", dependencies: [ .target(name: "App"), .product(name: "XCTVapor", package: "vapor"), .product(name: "Spec", package: "spec"), ]) ] ) ================================================ FILE: Chapter 15/myProject/Public/css/admin.css ================================================ tr { column-gap: 1rem; } ================================================ FILE: Chapter 15/myProject/Public/css/web.css ================================================ #blog h2 { margin: 0.5rem 0; } ================================================ FILE: Chapter 15/myProject/Public/js/admin.js ================================================ document.addEventListener("keydown", function(e) { if ( (window.navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey) && e.keyCode == 83 ) { e.preventDefault(); document.forms[0].submit(); } }, false); ================================================ FILE: Chapter 15/myProject/Public/js/web.js ================================================ function about() { alert("myPage\n\nversion 1.0.0"); } ================================================ FILE: Chapter 15/myProject/Sources/App/Extensions/Svg+MenuIcon.swift ================================================ import SwiftSvg extension Svg { static func menuIcon() -> Svg { Svg { Line(x1: 3, y1: 12, x2: 21, y2: 12) Line(x1: 3, y1: 6, x2: 21, y2: 6) Line(x1: 3, y1: 18, x2: 21, y2: 18) } .width(24) .height(24) .viewBox(minX: 0, minY: 0, width: 24, height: 24) .fill("none") .stroke("currentColor") .strokeWidth(2) .strokeLinecap("round") .strokeLinejoin("round") } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/ApiModelInterface.swift ================================================ import Vapor public protocol ApiModelInterface { associatedtype Module: ApiModuleInterface static var pathKey: String { get } static var pathIdKey: String { get } } extension ApiModelInterface { static var pathKey: String { String(describing: self).lowercased() + "s" } static var pathIdKey: String { String(describing: self).lowercased() + "Id" } static var pathIdComponent: PathComponent { .init(stringLiteral: ":" + pathIdKey) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/ApiModuleInterface.swift ================================================ import Foundation public protocol ApiModuleInterface { static var pathKey: String { get } } public extension ApiModuleInterface { static var pathKey: String { String(describing: self).lowercased() } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/AuthenticatedUser.swift ================================================ import Vapor public struct AuthenticatedUser { public let id: UUID public let email: String public init( id: UUID, email: String ) { self.id = id self.email = email } } extension AuthenticatedUser: SessionAuthenticatable { public var sessionID: UUID { id } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Controllers/CreateController.swift ================================================ import Vapor protocol CreateController: ModelController { func create( _ req: Request, _ model: DatabaseModel ) async throws func beforeCreate( _ req: Request, _ model: DatabaseModel ) async throws func afterCreate( _ req: Request, _ model: DatabaseModel ) async throws } extension CreateController { func create( _ req: Request, _ model: DatabaseModel ) async throws { try await beforeCreate(req, model) try await model.create(on: req.db) try await afterCreate(req, model) } func beforeCreate( _ req: Request, _ model: DatabaseModel ) async throws {} func afterCreate( _ req: Request, _ model: DatabaseModel ) async throws {} } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Controllers/DeleteController.swift ================================================ import Vapor public protocol DeleteController: ModelController { func delete( _ req: Request, _ model: DatabaseModel ) async throws func beforeDelete( _ req: Request, _ model: DatabaseModel ) async throws func afterDelete( _ req: Request, _ model: DatabaseModel ) async throws } public extension DeleteController { func delete( _ req: Request, _ model: DatabaseModel ) async throws { try await beforeDelete(req, model) try await model.delete(on: req.db) try await afterDelete(req, model) } func beforeDelete( _ req: Request, _ model: DatabaseModel ) async throws {} func afterDelete( _ req: Request, _ model: DatabaseModel ) async throws {} } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Controllers/DetailController.swift ================================================ import Vapor import Fluent protocol DetailController: ModelController { func detail( _ req: Request ) async throws -> DatabaseModel func beforeDetail( _ req: Request, _ queryBuilder: QueryBuilder ) async throws -> QueryBuilder func afterDetail( _ req: Request, _ model: DatabaseModel ) async throws -> DatabaseModel } extension DetailController { func detail( _ req: Request ) async throws -> DatabaseModel { let queryBuilder = DatabaseModel.query(on: req.db) let model = try await beforeDetail(req, queryBuilder) .filter(\._$id == identifier(req)) .first() guard let model = model else { throw Abort(.notFound) } return try await afterDetail(req, model) } func beforeDetail( _ req: Request, _ queryBuilder: QueryBuilder ) async throws -> QueryBuilder { queryBuilder } func afterDetail( _ req: Request, _ model: DatabaseModel ) async throws -> DatabaseModel { model } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Controllers/ListController.swift ================================================ import Vapor import Fluent protocol ListController: ModelController { func list( _ req: Request ) async throws -> [DatabaseModel] func beforeList( _ req: Request, _ queryBuilder: QueryBuilder ) async throws -> QueryBuilder func afterList( _ req: Request, _ models: [DatabaseModel] ) async throws -> [DatabaseModel] } extension ListController { func list( _ req: Request ) async throws -> [DatabaseModel] { try await DatabaseModel .query(on: req.db) .all() } func beforeList( _ req: Request, _ queryBuilder: QueryBuilder ) async throws -> QueryBuilder { queryBuilder } func afterList( _ req: Request, _ models: [DatabaseModel] ) async throws -> [DatabaseModel] { models } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Controllers/ModelController.swift ================================================ import Vapor import Fluent public struct Name { let singular: String let plural: String init( singular: String, plural: String? = nil ) { self.singular = singular self.plural = plural ?? singular + "s" } } public protocol ModelController { associatedtype ApiModel: ApiModelInterface associatedtype DatabaseModel: DatabaseModelInterface static var moduleName: String { get } static var modelName: Name { get } func identifier( _ req: Request ) throws -> UUID func findBy( _ id: UUID, on: Database ) async throws -> DatabaseModel func getBaseRoutes( _ routes: RoutesBuilder ) -> RoutesBuilder } extension ModelController { static var moduleName: String { DatabaseModel.Module.identifier.capitalized } static var modelName: Name { .init(singular: String(DatabaseModel.identifier.dropLast(1))) } func identifier( _ req: Request ) throws -> UUID { guard let id = req.parameters.get(ApiModel.pathIdKey), let uuid = UUID(uuidString: id) else { throw Abort(.badRequest) } return uuid } func findBy( _ id: UUID, on db: Database ) async throws -> DatabaseModel { guard let model = try await DatabaseModel.find(id, on: db) else { throw Abort(.notFound) } return model } func getBaseRoutes( _ routes: RoutesBuilder ) -> RoutesBuilder { routes .grouped(ApiModel.Module.pathKey.pathComponents) .grouped(ApiModel.pathKey.pathComponents) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Controllers/PatchController.swift ================================================ import Vapor public protocol PatchController: ModelController { func patch( _ req: Request, _ model: DatabaseModel ) async throws func beforePatch( _ req: Request, _ model: DatabaseModel ) async throws func afterPatch( _ req: Request, _ model: DatabaseModel ) async throws } public extension PatchController { func patch( _ req: Request, _ model: DatabaseModel ) async throws { try await beforePatch(req, model) try await model.update(on: req.db) try await afterPatch(req, model) } func beforePatch( _ req: Request, _ model: DatabaseModel ) async throws {} func afterPatch( _ req: Request, _ model: DatabaseModel ) async throws {} } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Controllers/UpdateController.swift ================================================ import Vapor public protocol UpdateController: ModelController { func update( _ req: Request, _ model: DatabaseModel ) async throws func beforeUpdate( _ req: Request, _ model: DatabaseModel ) async throws func afterUpdate( _ req: Request, _ model: DatabaseModel ) async throws } public extension UpdateController { func update( _ req: Request, _ model: DatabaseModel ) async throws { try await beforeUpdate(req, model) try await model.update(on: req.db) try await afterUpdate(req, model) } func beforeUpdate( _ req: Request, _ model: DatabaseModel ) async throws {} func afterUpdate( _ req: Request, _ model: DatabaseModel ) async throws {} } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/DatabaseModelInterface.swift ================================================ import Vapor import Fluent public protocol DatabaseModelInterface: Fluent.Model where Self.IDValue == UUID { associatedtype Module: ModuleInterface static var identifier: String { get } } public extension DatabaseModelInterface { static var schema: String { Module.identifier + "_" + identifier } static var identifier: String { String(describing: self) .dropFirst(Module.identifier.count) .dropLast(5) .lowercased() + "s" } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Extensions/ByteBuffer+Data.swift ================================================ import Vapor public extension ByteBuffer { var data: Data? { getData(at: 0, length: readableBytes) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Extensions/File+ByteBuffer.swift ================================================ import Vapor public extension File { var byteBuffer: ByteBuffer { data } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Form/AbstractForm.swift ================================================ import Vapor open class AbstractForm: FormComponent { open var action: FormAction open var fields: [FormComponent] open var error: String? open var submit: String? public init( action: FormAction = .init(), fields: [FormComponent] = [], error: String? = nil, submit: String? = nil ) { self.action = action self.fields = fields self.error = error self.submit = submit self.action.enctype = .multipart } open func load(req: Request) async throws { for field in fields { try await field.load(req: req) } } open func process(req: Request) async throws { for field in fields { try await field.process(req: req) } } open func validate(req: Request) async throws -> Bool { var result: [Bool] = [] for field in fields { result.append(try await field.validate(req: req)) } return result.filter { $0 == false }.isEmpty } open func write(req: Request) async throws { for field in fields { try await field.write(req: req) } } open func save(req: Request) async throws { for field in fields { try await field.save(req: req) } } open func read(req: Request) async throws { for field in fields { try await field.read(req: req) } } open func render(req: Request) -> TemplateRepresentable { FormTemplate(getContext(req)) } func getContext(_ req: Request) -> FormContext { .init( action: action, fields: fields.map { $0.render(req: req)}, error: error, submit: submit ) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Form/AbstractFormField.swift ================================================ import Vapor open class AbstractFormField< Input: Decodable, Output: TemplateRepresentable >: FormComponent { public var key: String public var input: Input public var output: Output public var error: String? // MARK: - event blocks public typealias FormFieldBlock = (Request, AbstractFormField) async throws -> Void public typealias FormFieldValidatorsBlock = ((Request, AbstractFormField) -> [AsyncValidator]) private var readBlock: FormFieldBlock? private var writeBlock: FormFieldBlock? private var loadBlock: FormFieldBlock? private var saveBlock: FormFieldBlock? private var validatorsBlock: FormFieldValidatorsBlock? // MARK: - init & config public init( key: String, input: Input, output: Output, error: String? = nil ) { self.key = key self.input = input self.output = output self.error = error } open func config( _ block: (AbstractFormField) -> Void ) -> Self { block(self) return self } // MARK: - Block setters open func read(_ block: @escaping FormFieldBlock) -> Self { readBlock = block return self } open func write(_ block: @escaping FormFieldBlock) -> Self { writeBlock = block return self } open func load(_ block: @escaping FormFieldBlock) -> Self { loadBlock = block return self } open func save(_ block: @escaping FormFieldBlock) -> Self { saveBlock = block return self } open func validators( @AsyncValidatorBuilder _ block: @escaping FormFieldValidatorsBlock ) -> Self { validatorsBlock = block return self } // MARK: - FormComponent open func process(req: Request) async throws { if let value = try? req.content.get(Input.self, at: key) { input = value } } open func validate(req: Request) async throws -> Bool { guard let validators = validatorsBlock else { return true } return await RequestValidator(validators(req, self)).isValid(req) } open func read(req: Request) async throws { try await readBlock?(req, self) } open func write(req: Request) async throws { try await writeBlock?(req, self) } open func load(req: Request) async throws { try await loadBlock?(req, self) } open func save(req: Request) async throws { try await saveBlock?(req, self) } open func render(req: Request) -> TemplateRepresentable { output } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Form/Fields/HiddenField.swift ================================================ import Vapor public final class HiddenField: AbstractFormField< String, HiddenFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Form/Fields/ImageField.swift ================================================ import Vapor public final class ImageField: AbstractFormField< FormImageInput, ImageFieldTemplate > { public var imageKey: String? { didSet { output.context.data.originalKey = imageKey } } public var path: String public init( _ key: String, path: String ) { self.path = path super.init( key: key, input: .init(key: key), output: .init(.init(key: key)) ) } public override func process( req: Request ) async throws { /// process input input.file = try? req.content.get( File.self, at: key ) input.data.originalKey = try? req.content.get( String.self, at: key + "OriginalKey" ) if let temporaryFileKey = try? req.content.get( String.self, at: key + "TemporaryFileKey" ), let temporaryFileName = try? req.content.get( String.self, at: key + "TemporaryFileName" ) { input.data.temporaryFile = .init( key: temporaryFileKey, name: temporaryFileName ) } input.data.shouldRemove = (try? req.content.get( Bool.self, at: key + "ShouldRemove" )) ?? false /// remove & upload file if input.data.shouldRemove { if let originalKey = input.data.originalKey { try? await req.fs.delete(key: originalKey) } } else if let file = input.file, let data = file.byteBuffer.data, !data.isEmpty { if let tmpKey = input.data.temporaryFile?.key { try? await req.fs.delete(key: tmpKey) } let key = "tmp/\(UUID().uuidString).tmp" _ = try await req.fs.upload(key: key, data: data) /// update the temporary image input.data.temporaryFile = .init( key: key, name: file.filename ) } /// update output values output.context.data = input.data } public override func write( req: Request ) async throws { imageKey = input.data.originalKey if input.data.shouldRemove { if let key = input.data.originalKey { try? await req.fs.delete(key: key) } imageKey = nil } else if let file = input.data.temporaryFile { var newKey = path + "/" + file.name if await req.fs.exists(key: newKey) { let formatter = DateFormatter() formatter.dateFormat="y-MM-dd-HH-mm-ss-" let prefix = formatter.string(from: .init()) newKey = path + "/" + prefix + file.name } _ = try await req.fs.move(key: file.key, to: newKey) input.data.temporaryFile = nil if let key = input.data.originalKey { try? await req.fs.delete(key: key) } imageKey = newKey } try await super.write(req: req) } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Form/Fields/InputField.swift ================================================ public final class InputField: AbstractFormField< String, InputFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init( key: key ) ) ) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Form/Fields/SelectField.swift ================================================ import Vapor public final class SelectField: AbstractFormField< String, SelectFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Form/Fields/TextareaField.swift ================================================ import Vapor public final class TextareaField: AbstractFormField< String, TextareaFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Form/FormAction.swift ================================================ import SwiftHtml public struct FormAction { public var method: SwiftHtml.Method public var url: String? public var enctype: SwiftHtml.Enctype? public init( method: SwiftHtml.Method = .post, url: String? = nil, enctype: SwiftHtml.Enctype? = nil ) { self.method = method self.url = url self.enctype = enctype } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Form/FormComponent.swift ================================================ import Vapor public protocol FormComponent { func load(req: Request) async throws func process(req: Request) async throws func validate(req: Request) async throws -> Bool func write(req: Request) async throws func save(req: Request) async throws func read(req: Request) async throws func render(req: Request) -> TemplateRepresentable } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Form/FormComponentBuilder.swift ================================================ @resultBuilder public enum FormComponentBuilder { public static func buildBlock( _ components: FormComponent... ) -> [FormComponent] { components } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Form/FormImageData.swift ================================================ public struct FormImageData: Codable { public struct TemporaryFile: Codable { public let key: String public let name: String public init( key: String, name: String ) { self.key = key self.name = name } } public var originalKey: String? public var temporaryFile: TemporaryFile? public var shouldRemove: Bool public init( originalKey: String? = nil, temporaryFile: TemporaryFile? = nil, shouldRemove: Bool = false ) { self.originalKey = originalKey self.temporaryFile = temporaryFile self.shouldRemove = shouldRemove } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Form/FormImageInput.swift ================================================ import Vapor public struct FormImageInput: Codable { public var key: String public var file: File? public var data: FormImageData public init( key: String, file: File? = nil, data: FormImageData? = nil ) { self.key = key self.file = file self.data = data ?? .init() } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Hooks/Application+HookStorage.swift ================================================ import Vapor extension Application { private struct HookStorageKey: StorageKey { typealias Value = HookStorage } var hooks: HookStorage { get { if let existing = storage[HookStorageKey.self] { return existing } let new = HookStorage() storage[HookStorageKey.self] = new return new } set { storage[HookStorageKey.self] = newValue } } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Hooks/Async/Application+AsyncHooks.swift ================================================ import Vapor extension Application { func invokeAsync( _ name: String, args: HookArguments = [:] ) async throws -> ReturnType? { let ctxArgs = args.merging(["app": self]) { (_, new) in new } return try await hooks.invokeAsync(name, args: ctxArgs) } func invokeAllAsync( _ name: String, args: HookArguments = [:] ) async throws -> [ReturnType] { let ctxArgs = args.merging(["app": self]) { (_, new) in new } return try await hooks.invokeAllAsync(name, args: ctxArgs) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Hooks/Async/AsyncAnyHookFunction.swift ================================================ struct AsyncAnyHookFunction: AsyncHookFunction { private let functionBlock: AsyncHookFunctionSignature init(_ functionBlock: @escaping AsyncHookFunctionSignature) { self.functionBlock = functionBlock } func invokeAsync(_ args: HookArguments) async throws -> Any { try await functionBlock(args) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Hooks/Async/AsyncHookFunction.swift ================================================ protocol AsyncHookFunction { func invokeAsync(_: HookArguments) async throws -> Any } typealias AsyncHookFunctionSignature = (HookArguments) async throws -> T ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Hooks/Async/HookStorage+AsyncHooks.swift ================================================ extension HookStorage { func registerAsync( _ name: String, use block: @escaping AsyncHookFunctionSignature ) { let function = AsyncAnyHookFunction { args -> Any in try await block(args) } let pointer = HookFunctionPointer( name: name, function: function, returnType: ReturnType.self ) asyncPointers.append(pointer) } func invokeAsync( _ name: String, args: HookArguments = [:] ) async throws -> ReturnType? { try await asyncPointers.first { $0.name == name && $0.returnType == ReturnType.self }?.pointer.invokeAsync(args) as? ReturnType } func invokeAllAsync( _ name: String, args: HookArguments = [:] ) async throws -> [ReturnType] { let fns = asyncPointers.filter { $0.name == name && $0.returnType == ReturnType.self } var result: [ReturnType] = [] for fn in fns { if let res = try await fn.pointer.invokeAsync(args) as? ReturnType { result.append(res) } } return result } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Hooks/Async/Request+AsyncHooks.swift ================================================ import Vapor extension Request { func invokeAsync( _ name: String, args: HookArguments = [:] ) async throws -> ReturnType? { let ctxArgs = args.merging(["req": self]) { (_, new) in new } return try await application.invokeAsync(name, args: ctxArgs) } func invokeAllAsync( _ name: String, args: HookArguments = [:] ) async throws -> [ReturnType] { let ctxArgs = args.merging(["req": self]) { (_, new) in new } return try await application.invokeAllAsync(name, args: ctxArgs) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Hooks/HookArguments.swift ================================================ typealias HookArguments = [String: Any] ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Hooks/HookFunctionPointer.swift ================================================ final class HookFunctionPointer { var name: String var pointer: Pointer var returnType: Any.Type init( name: String, function: Pointer, returnType: Any.Type ) { self.name = name self.pointer = function self.returnType = returnType } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Hooks/HookStorage.swift ================================================ final class HookStorage { var pointers: [HookFunctionPointer] var asyncPointers: [HookFunctionPointer] init() { self.pointers = [] self.asyncPointers = [] } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Hooks/Sync/AnyHookFunction.swift ================================================ struct AnyHookFunction: HookFunction { private let functionBlock: HookFunctionSignature init(_ functionBlock: @escaping HookFunctionSignature) { self.functionBlock = functionBlock } func invoke(_ args: HookArguments) -> Any { functionBlock(args) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Hooks/Sync/Application+Hooks.swift ================================================ import Vapor extension Application { func invoke( _ name: String, args: HookArguments = [:] ) -> ReturnType? { let ctxArgs = args.merging(["app": self]) { (_, new) in new } return hooks.invoke(name, args: ctxArgs) } func invokeAll( _ name: String, args: HookArguments = [:] ) -> [ReturnType] { let ctxArgs = args.merging(["app": self]) { (_, new) in new } return hooks.invokeAll(name, args: ctxArgs) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Hooks/Sync/HookFunction.swift ================================================ protocol HookFunction { func invoke(_: HookArguments) -> Any } typealias HookFunctionSignature = (HookArguments) -> T ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Hooks/Sync/HookStorage+Hooks.swift ================================================ extension HookStorage { func register( _ name: String, use block: @escaping HookFunctionSignature ) { let function = AnyHookFunction { args -> Any in block(args) } let pointer = HookFunctionPointer( name: name, function: function, returnType: ReturnType.self ) pointers.append(pointer) } func invoke( _ name: String, args: HookArguments = [:] ) -> ReturnType? { pointers.first { $0.name == name && $0.returnType == ReturnType.self }?.pointer.invoke(args) as? ReturnType } func invokeAll( _ name: String, args: HookArguments = [:] ) -> [ReturnType] { pointers.filter { $0.name == name && $0.returnType == ReturnType.self } .compactMap { $0.pointer.invoke(args) as? ReturnType } } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Hooks/Sync/Request+Hooks.swift ================================================ import Vapor extension Request { func invoke( _ name: String, args: HookArguments = [:] ) -> ReturnType? { let ctxArgs = args.merging(["req": self]) { (_, new) in new } return application.invoke(name, args: ctxArgs) } func invokeAll( _ name: String, args: HookArguments = [:] ) -> [ReturnType] { let ctxArgs = args.merging(["req": self]) { (_, new) in new } return application.invokeAll(name, args: ctxArgs) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/ModelEditorInterface.swift ================================================ import Vapor public protocol ModelEditorInterface: FormComponent { associatedtype Model: DatabaseModelInterface var model: Model { get } var form: AbstractForm { get } init(model: Model, form: AbstractForm) @FormComponentBuilder var formFields: [FormComponent] { get } } public extension ModelEditorInterface { func load(req: Request) async throws { try await form.load(req: req) } func process(req: Request) async throws { try await form.process(req: req) } func validate(req: Request) async throws -> Bool { try await form.validate(req: req) } func write(req: Request) async throws { try await form.write(req: req) } func save(req: Request) async throws { try await form.save(req: req) } func read(req: Request) async throws { try await form.read(req: req) } func render(req: Request) -> TemplateRepresentable { form.render(req: req) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/ModuleInterface.swift ================================================ import Vapor public protocol ModuleInterface { static var identifier: String { get } func boot(_ app: Application) throws func setUp(_ app: Application) throws } public extension ModuleInterface { func boot(_ app: Application) throws {} func setUp(_ app: Application) throws {} static var identifier: String { String(describing: self).dropLast(6).lowercased() } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Contexts/CellContext.swift ================================================ public struct CellContext { public enum `Type`: String { case text case image } public let value: String public let link: LinkContext? public let type: `Type` public init( _ value: String, link: LinkContext? = nil, type: `Type` = .text ) { self.type = type self.value = value self.link = link } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Contexts/ColumnContext.swift ================================================ public struct ColumnContext { public let key: String public let label: String public init( _ key: String, label: String? = nil ) { self.key = key self.label = label ?? key.capitalized } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Contexts/DetailContext.swift ================================================ public struct DetailContext { public enum `Type`: String { case text case image } public let key: String public let label: String public let value: String public let type: `Type` public init( _ key: String, _ value: String, label: String? = nil, type: `Type` = .text ) { self.key = key self.label = label ?? key.capitalized self.value = value self.type = type } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Contexts/FormContext.swift ================================================ public struct FormContext { public var action: FormAction public var fields: [TemplateRepresentable] public var error: String? public var submit: String? } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Contexts/HiddenFieldContext.swift ================================================ public struct HiddenFieldContext { public let key: String public var value: String? public init( key: String, value: String? = nil ) { self.key = key self.value = value } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Contexts/ImageFieldContext.swift ================================================ public struct ImageFieldContext { public let key: String public var label: LabelContext public var data: FormImageData public var previewUrl: String? public var accept: String? public var error: String? public init( key: String, label: LabelContext? = nil, data: FormImageData = .init(), previewUrl: String? = nil, accept: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.data = data self.previewUrl = previewUrl self.accept = accept self.error = error } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Contexts/InputFieldContext.swift ================================================ import SwiftHtml public struct InputFieldContext { public let key: String public var label: LabelContext public var type: SwiftHtml.Input.`Type` public var placeholder: String? public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, type: Input.`Type` = .text, placeholder: String? = nil, value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.type = type self.placeholder = placeholder self.value = value self.error = error } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Contexts/LabelContext.swift ================================================ public struct LabelContext { public var key: String public var title: String? public var required: Bool public var more: String? public init( key: String, title: String? = nil, required: Bool = false, more: String? = nil ) { self.key = key self.title = title self.required = required self.more = more } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Contexts/LinkContext.swift ================================================ import Vapor public struct LinkContext { public let label: String public let path: String public let absolute: Bool public let isBlank: Bool public let dropLast: Int public init( label: String, path: String = "", absolute: Bool = false, isBlank: Bool = false, dropLast: Int = 0 ) { self.label = label self.path = path self.absolute = absolute self.isBlank = isBlank self.dropLast = dropLast } public func url( _ req: Request, _ infix: [PathComponent] = [] ) -> String { if absolute { return path } return "/" + (req.url.path.pathComponents.dropLast(dropLast) + (infix + path.pathComponents)).string } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Contexts/OptionContext.swift ================================================ public struct OptionContext { public var key: String public var label: String public init( key: String, label: String ) { self.key = key self.label = label } } public extension OptionContext { static func yesNo() -> [OptionContext] { ["yes", "no"].map { .init(key: $0, label: $0.capitalized) } } static func trueFalse() -> [OptionContext] { [true, false].map { .init(key: String($0), label: String($0).capitalized) } } static func numbers( _ numbers: [Int] ) -> [OptionContext] { numbers.map { .init(key: String($0), label: String($0)) } } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Contexts/RowContext.swift ================================================ public struct RowContext { public let id: String public let cells: [CellContext] public init( id: String, cells: [CellContext] ) { self.id = id self.cells = cells } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Contexts/SelectFieldContext.swift ================================================ public struct SelectFieldContext { public let key: String public var label: LabelContext public var options: [OptionContext] public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, options: [OptionContext] = [], value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.options = options self.value = value self.error = error } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Contexts/TableContext.swift ================================================ public struct TableContext { public let columns: [ColumnContext] public let rows: [RowContext] public let actions: [LinkContext] public init( columns: [ColumnContext], rows: [RowContext], actions: [LinkContext] = [] ) { self.columns = columns self.rows = rows self.actions = actions } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Contexts/TextareaFieldContext.swift ================================================ public struct TextareaFieldContext { public let key: String public var label: LabelContext public var placeholder: String? public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, placeholder: String? = nil, value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.placeholder = placeholder self.value = value self.error = error } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Html/CellTemplate.swift ================================================ import Vapor import SwiftHtml public struct CellTemplate: TemplateRepresentable { var context: CellContext var rowId: String public init( _ context: CellContext, rowId: String ) { self.context = context self.rowId = rowId } @TagBuilder public func render( _ req: Request ) -> Tag { Td { switch context.type { case .text: if let link = context.link { LinkTemplate(link, pathInfix: rowId).render(req) } else { Text(context.value) } case .image: if let link = context.link { LinkTemplate(link, pathInfix: rowId) { label in Img(src: context.value, alt: label) } .render(req) } else { Img(src: context.value, alt: context.value) } } } .class("field") } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Html/DetailTemplate.swift ================================================ import Vapor import SwiftHtml public struct DetailTemplate: TemplateRepresentable { var context: DetailContext public init( _ context: DetailContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Dt(context.label) switch context.type { case .text: if context.value.isEmpty { Dd(" ") } else { Dd( context.value.replacingOccurrences( of: "\n", with: "
" ) ) } case .image: Dd { Img( src: context.value, alt: context.label ) } } } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Html/FormTemplate.swift ================================================ import Vapor import SwiftHtml public struct FormTemplate: TemplateRepresentable { var context: FormContext public init( _ context: FormContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Form { if let error = context.error { Section { P(error) .class("error") } } for field in context.fields { Section { field.render(req) } } Section { Input() .type(.submit) .value(context.submit ?? "Save") } } .method(context.action.method) .action(context.action.url) .enctype(context.action.enctype) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Html/HiddenFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct HiddenFieldTemplate: TemplateRepresentable { var context: HiddenFieldContext public init( _ context: HiddenFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Input() .type(.hidden) .name(context.key) .value(context.value) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Html/ImageFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct ImageFieldTemplate: TemplateRepresentable { public var context: ImageFieldContext public init( _ context: ImageFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { if let url = context.previewUrl { Img(src: req.fs.resolve(key: url), alt: context.key) } else { Img(src: "/img/logo.png", alt: "default post image") } LabelTemplate(context.label).render(req) Input() .type(.file) .key(context.key) .class("field") .accept(context.accept) if let temporaryFile = context.data.temporaryFile { Input() .key(context.key + "TemporaryFileKey") .value(temporaryFile.key) .type(.hidden) Input() .key(context.key + "TemporaryFileName") .value(temporaryFile.name) .type(.hidden) } if let key = context.data.originalKey { Input() .key(context.key + "OriginalKey") .value(key) .type(.hidden) } if !context.label.required { Input() .key(context.key + "ShouldRemove") .value(String(true)) .type(.checkbox) .checked(context.data.shouldRemove) Label("Remove") .for(context.key + "Remove") } if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Html/InputFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct InputFieldTemplate: TemplateRepresentable { public var context: InputFieldContext public init( _ context: InputFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Input() .type(context.type) .key(context.key) .placeholder(context.placeholder) .value(context.value) .class("field") if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Html/LabelTemplate.swift ================================================ import Vapor import SwiftHtml public struct LabelTemplate: TemplateRepresentable { var context: LabelContext public init( _ context: LabelContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Label { Text(context.title ?? context.key.capitalized) if let more = context.more { Span(more) .class("more") } if context.required { Span("*") .class("required") } }.for(context.key) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Html/LinkTemplate.swift ================================================ import Vapor import SwiftHtml public struct LinkTemplate: TemplateRepresentable { var context: LinkContext var body: Tag var pathInfix: String? public init( _ context: LinkContext, pathInfix: String? = nil, _ builder: ((String) -> Tag)? = nil ) { self.context = context self.pathInfix = pathInfix self.body = builder?(context.label) ?? Text(context.label) } @TagBuilder public func render( _ req: Request ) -> Tag { A { body } .href(context.url(req, pathInfix?.pathComponents ?? [])) .target(.blank, context.isBlank) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Html/SelectFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct SelectFieldTemplate: TemplateRepresentable { public var context: SelectFieldContext public init( _ context: SelectFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Select { for item in context.options { Option(item.label) .value(item.key) .selected(context.value == item.key) } } .name(context.key) if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Html/TableTemplate.swift ================================================ import Vapor import SwiftHtml public struct TableTemplate: TemplateRepresentable { var context: TableContext public init( _ context: TableContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Table { Thead { Tr { context.columns.map { column in Th(column.label) .id(column.key) .class("field") } context.actions.map { action in Th(action.label) .class("action") } } } Tbody { for row in context.rows { Tr { row.cells.map { CellTemplate($0, rowId: row.id).render(req) } context.actions.map { action in Td { LinkTemplate(action, pathInfix: row.id).render(req) } .class("action") } } .id(row.id) } } } } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Templates/Html/TextareaFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct TextareaFieldTemplate: TemplateRepresentable { public var context: TextareaFieldContext public init( _ context: TextareaFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Textarea(context.value) .placeholder(context.placeholder) .name(context.key) if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Validation/AsyncValidator.swift ================================================ import Vapor public protocol AsyncValidator { var key: String { get } var message: String { get } func validate( _ req: Request ) async throws -> ValidationErrorDetail? } public extension AsyncValidator { var error: ValidationErrorDetail { .init(key: key, message: message) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Validation/AsyncValidatorBuilder.swift ================================================ @resultBuilder public enum AsyncValidatorBuilder { public static func buildBlock( _ components: AsyncValidator... ) -> [AsyncValidator] { components } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Validation/FormFieldValidator+Validations.swift ================================================ import Vapor public extension FormFieldValidator where Input == String { static func required( _ field: AbstractFormField, _ message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is required" return .init(field, msg) { _, field in !field.input.isEmpty } } static func min( _ field: AbstractFormField, length: Int, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)" return .init(field, msg) { _, field in field.input.count >= length } } static func max( _ field: AbstractFormField, length: Int, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)" return .init(field, msg) { _, field in field.input.count <= length } } static func alphanumeric( _ field: AbstractFormField, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) should be only alphanumeric characters" return .init(field, msg) { _, field in !Validator .characterSet(.alphanumerics) .validate(field.input) .isFailure } } static func email( _ field: AbstractFormField, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) should be a valid email address" return .init(field, msg) { _, field in !Validator .email .validate(field.input) .isFailure } } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Validation/FormFieldValidator.swift ================================================ import Vapor public struct FormFieldValidator< Input: Decodable, Output: TemplateRepresentable >: AsyncValidator { public typealias AsyncValidationBlock = ((Request, AbstractFormField) async throws -> Bool) public let field: AbstractFormField public let message: String public let validation: AsyncValidationBlock public var key: String { field.key } public init( _ field: AbstractFormField, _ message: String, _ validation: @escaping AsyncValidationBlock ) { self.field = field self.message = message self.validation = validation } public func validate( _ req: Request ) async throws -> ValidationErrorDetail? { let isValid = try await validation(req, field) if isValid { return nil } field.error = message return error } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Validation/KeyedContentValidator+Validations.swift ================================================ import Vapor public extension KeyedContentValidator where T == String { static func required( _ key: String, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is required" return .init(key, msg, optional: optional) { value, _ in !value.isEmpty } } static func min( _ key: String, _ length: Int, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is too short (min: \(length) characters)" return .init(key, msg, optional: optional) { value, _ in value.count >= length } } static func max( _ key: String, _ length: Int, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is too long (max: \(length) characters)" return .init(key, msg, optional: optional) { value, _ in value.count <= length } } static func alphanumeric( _ key: String, _ message: String? = nil, _ optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) should be only alphanumeric characters" return .init(key, msg, optional: optional) { value, _ in !Validator.characterSet(.alphanumerics).validate(value).isFailure } } static func email( _ key: String, _ message: String? = nil, _ optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) should be a valid email address" return .init(key, msg, optional: optional) { value, _ in !Validator.email.validate(value).isFailure } } } public extension KeyedContentValidator where T == Int { static func min( _ key: String, _ length: Int, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is too short (min: \(length) characters)" return .init(key, msg, optional: optional) { value, _ in value >= length } } static func max( _ key: String, _ length: Int, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is too long (max: \(length) characters)" return .init(key, msg, optional: optional) { value, _ in value <= length } } static func contains( _ key: String, _ values: [Int], _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is an invalid value" return .init(key, msg, optional: optional) { value, _ in values.contains(value) } } } public extension KeyedContentValidator where T == UUID { static func required( _ key: String, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is required" return .init(key, msg, optional: optional) { _, _ in true } } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Validation/KeyedContentValidator.swift ================================================ import Vapor public struct KeyedContentValidator: AsyncValidator { public let key: String public let message: String public let optional: Bool public let validation: (T, Request) async throws -> Bool public init( _ key: String, _ message: String, optional: Bool = false, _ validation: @escaping (T, Request) async throws -> Bool ) { self.key = key self.message = message self.optional = optional self.validation = validation } public func validate( _ req: Request ) async throws -> ValidationErrorDetail? { let optionalValue = try? req.content.get(T.self, at: key) if let value = optionalValue { return try await validation(value, req) ? nil : error } return optional ? nil : error } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Validation/RequestValidator.swift ================================================ import Vapor public struct RequestValidator { public var validators: [AsyncValidator] public init( _ validators: [AsyncValidator] ) { self.validators = validators } public func validate( _ req: Request, message: String? = nil ) async throws { var result: [ValidationErrorDetail] = [] for validator in validators { if result.contains(where: { $0.key == validator.key }) { continue } if let res = try await validator.validate(req) { result.append(res) } } if !result.isEmpty { throw ValidationAbort( abort: Abort(.badRequest, reason: message), details: result ) } } public func isValid( _ req: Request ) async -> Bool { do { try await validate(req, message: nil) return true } catch { return false } } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Validation/ValidationAbort.swift ================================================ import Vapor public struct ValidationAbort: AbortError { public var abort: Abort public var message: String? public var details: [ValidationErrorDetail] public var reason: String { abort.reason } public var status: HTTPStatus { abort.status } public init( abort: Abort, message: String? = nil, details: [ValidationErrorDetail] ) { self.abort = abort self.message = message self.details = details } } ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Validation/ValidationError.swift ================================================ import Vapor struct ValidationError: Codable { let message: String? let details: [ValidationErrorDetail] init( message: String?, details: [ValidationErrorDetail] ) { self.message = message self.details = details } } extension ValidationError: Content {} ================================================ FILE: Chapter 15/myProject/Sources/App/Framework/Validation/ValidationErrorDetail.swift ================================================ import Vapor public struct ValidationErrorDetail: Codable { public var key: String public var message: String public init( key: String, message: String ) { self.key = key self.message = message } } extension ValidationErrorDetail: Content {} ================================================ FILE: Chapter 15/myProject/Sources/App/Middlewares/ExtendPathMiddleware.swift ================================================ import Vapor struct ExtendPathMiddleware: AsyncMiddleware { func respond( to req: Request, chainingTo next: AsyncResponder ) async throws -> Response { if !req.url.path.hasSuffix("/") && !req.url.path.contains(".") { return req.redirect( to: req.url.path + "/", redirectType: .permanent ) } return try await next.respond( to: req ) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Admin/AdminModule.swift ================================================ import Vapor struct AdminModule: ModuleInterface { let router = AdminRouter() func boot(_ app: Application) throws { try router.boot(routes: app.routes) app.hooks.register("admin-routes", use: router.adminRoutesHook) } func setUp(_ app: Application) throws { try router.setUpRoutesHooks(app: app) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Admin/AdminRouter.swift ================================================ import Vapor struct AdminRouter: RouteCollection { let controller = AdminFrontendController() func boot(routes: RoutesBuilder) throws {} func setUpRoutesHooks(app: Application) throws { let adminRoutes = app.routes .grouped( AuthenticatedUser.redirectMiddleware( path: "/sign-in/" ) ) .grouped("admin") let _: [Void] = app.invokeAll( "admin-routes", args: ["routes": adminRoutes] ) } func adminRoutesHook(_ args: HookArguments) -> Void { let routes = args["routes"] as! RoutesBuilder routes.get(use: controller.dashboardView) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Admin/Controllers/AdminController.swift ================================================ import Vapor protocol AdminController: AdminListController, AdminDetailController, AdminCreateController, AdminUpdateController, AdminDeleteController { func setupRoutes( _ routes: RoutesBuilder ) } extension AdminController { func setupRoutes( _ routes: RoutesBuilder ) { setupListRoutes(routes) setupDetailRoutes(routes) setupCreateRoutes(routes) setupUpdateRoutes(routes) setupDeleteRoutes(routes) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Admin/Controllers/AdminCreateController.swift ================================================ import Vapor protocol AdminCreateController: CreateController { associatedtype CreateModelEditor: ModelEditorInterface func createTemplate( _ req: Request, _ editor: CreateModelEditor ) -> TemplateRepresentable func createView( _ req: Request ) async throws -> Response func createAction( _ req: Request ) async throws -> Response func createContext( _ req: Request, _ editor: CreateModelEditor ) -> AdminEditorPageContext func createBreadcrumbs( _ req: Request ) -> [LinkContext] func setupCreateRoutes( _ routes: RoutesBuilder ) } extension AdminCreateController { private func render( _ req: Request, editor: CreateModelEditor ) -> Response { req.templates.renderHtml( createTemplate(req, editor) ) } func createView( _ req: Request ) async throws -> Response { let editor = CreateModelEditor( model: .init(), form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) return render(req, editor: editor) } func createAction( _ req: Request ) async throws -> Response { let model = DatabaseModel() let editor = CreateModelEditor( model: model as! CreateModelEditor.Model, form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) try await editor.process(req: req) let isValid = try await editor.validate(req: req) guard isValid else { return render(req, editor: editor) } try await editor.write(req: req) try await create(req, editor.model as! DatabaseModel) try await editor.save(req: req) var components = req.url.path.pathComponents.dropLast() components += editor.model.id!.uuidString.pathComponents return req.redirect(to: "/" + components.string + "/update/") } func createTemplate( _ req: Request, _ editor: CreateModelEditor ) -> TemplateRepresentable { AdminEditorPageTemplate( createContext(req, editor) ) } func createContext( _ req: Request, _ editor: CreateModelEditor ) -> AdminEditorPageContext { let context = FormContext( action: editor.form.action, fields: editor.form.fields.map { $0.render(req: req) }, error: editor.form.error, submit: editor.form.submit ) return .init( title: "Create", form: context, breadcrumbs: createBreadcrumbs(req) ) } func createBreadcrumbs( _ req: Request ) -> [LinkContext] { [ LinkContext( label: Self.moduleName.capitalized, dropLast: 2 ), LinkContext( label: Self.modelName.plural.capitalized, dropLast: 1 ), ] } func setupCreateRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) baseRoutes.get("create", use: createView) baseRoutes.post("create", use: createAction) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Admin/Controllers/AdminDeleteController.swift ================================================ import Vapor final class DeleteForm: AbstractForm { init() { super.init() self.action.method = .post self.submit = "Delete" } } protocol AdminDeleteController: DeleteController { func deleteView( _ req: Request ) async throws -> Response func deleteAction( _ req: Request ) async throws -> Response func deleteTemplate( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> TemplateRepresentable func deleteInfo( _ model: DatabaseModel ) -> String func deleteContext( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> AdminDeletePageContext func setupDeleteRoutes( _ routes: RoutesBuilder ) } extension AdminDeleteController { func deleteView( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let form = DeleteForm() return req.templates.renderHtml( deleteTemplate(req, model, form) ) } func deleteAction( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) try await delete(req, model) var url = req.url.path if let redirect = try? req.query.get(String.self, at: "redirect") { url = redirect } return req.redirect(to: url) } func deleteTemplate( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> TemplateRepresentable { AdminDeletePageTemplate( deleteContext(req, model, form) ) } func deleteContext( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> AdminDeletePageContext { .init( title: "Delete", name: deleteInfo(model), type: "model", form: form.getContext(req) ) } func setupDeleteRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.get("delete", use: deleteView) existingModelRoutes.post("delete", use: deleteAction) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Admin/Controllers/AdminDetailController.swift ================================================ import Vapor protocol AdminDetailController: DetailController { func detailView( _ req: Request ) async throws -> Response func detailTemplate( _ req: Request, _ model: DatabaseModel ) -> TemplateRepresentable func detailFields( for model: DatabaseModel ) -> [DetailContext] func detailContext( _ req: Request, _ model: DatabaseModel ) -> AdminDetailPageContext func detailBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func detailNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func setupDetailRoutes( _ routes: RoutesBuilder ) } extension AdminDetailController { func detailView( _ req: Request ) async throws -> Response { let model = try await detail(req) return req.templates.renderHtml(detailTemplate(req, model)) } func detailTemplate( _ req: Request, _ model: DatabaseModel ) -> TemplateRepresentable { AdminDetailPageTemplate(detailContext(req, model)) } func detailContext( _ req: Request, _ model: DatabaseModel ) -> AdminDetailPageContext { let path = "/delete/?redirect=" + req.url.path.pathComponents.dropLast().string + "&cancel=" + req.url.path return .init( title: "Details", fields: detailFields(for: model), navigation: detailNavigation(req, model), breadcrumbs: detailBreadcrumbs(req, model), actions: [ LinkContext( label: "Delete", path: path ), ] ) } func detailBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: Self.moduleName.capitalized, dropLast: 2 ), LinkContext( label: Self.modelName.plural.capitalized, dropLast: 1 ), ] } func detailNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: "Update", path: "update" ), ] } func setupDetailRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.get(use: detailView) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Admin/Controllers/AdminFrontendController.swift ================================================ import Vapor struct AdminFrontendController { func dashboardView( req: Request ) throws -> Response { let user = try req.auth.require(AuthenticatedUser.self) let template = AdminDashboardTemplate( .init( icon: "👋", title: "Dashboard", message: "Hello \(user.email), welcome to the CMS." ) ) return req.templates.renderHtml(template) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Admin/Controllers/AdminListController.swift ================================================ import Vapor protocol AdminListController: ListController { func listView( _ req: Request ) async throws -> Response func listColumns() -> [ColumnContext] func listCells( for model: DatabaseModel ) -> [CellContext] func listNavigation( _ req: Request ) -> [LinkContext] func listBreadcrumbs( _ req: Request ) -> [LinkContext] func listContext( _ req: Request, _ list: [DatabaseModel] ) -> AdminListPageContext func listTemplate( _ req: Request, _ list: [DatabaseModel] ) -> TemplateRepresentable func setupListRoutes( _ routes: RoutesBuilder ) } extension AdminListController { func listView(_ req: Request) async throws -> Response { let list = try await list(req) let template = listTemplate(req, list) return req.templates.renderHtml(template) } func listNavigation(_ req: Request) -> [LinkContext] { [ LinkContext(label: "Create",path: "create") ] } func listBreadcrumbs(_ req: Request) -> [LinkContext] { [ LinkContext( label: DatabaseModel.Module.identifier.capitalized, dropLast: 1 ) ] } func listContext( _ req: Request, _ list: [DatabaseModel] ) -> AdminListPageContext { let rows = list.map { RowContext(id: $0.id!.uuidString, cells: listCells(for: $0)) } let table = TableContext(columns: listColumns(), rows: rows, actions: [ LinkContext(label: "Update", path: "update"), LinkContext(label: "Delete", path: "delete") ]) return .init( title: "List", table: table, navigation: listNavigation(req), breadcrumbs: listBreadcrumbs(req) ) } func listTemplate( _ req: Request, _ list: [DatabaseModel] ) -> TemplateRepresentable { AdminListPageTemplate(listContext(req, list)) } func setupListRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) baseRoutes.get(use: listView) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Admin/Controllers/AdminUpdateController.swift ================================================ import Vapor protocol AdminUpdateController: UpdateController { associatedtype UpdateModelEditor: ModelEditorInterface func updateView( _ req: Request ) async throws -> Response func updateAction( _ req: Request ) async throws -> Response func updateTemplate( _ req: Request, _ editor: UpdateModelEditor ) async -> TemplateRepresentable func updateContext( _ req: Request, _ editor: UpdateModelEditor ) async -> AdminEditorPageContext func updateBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func updateNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func setupUpdateRoutes( _ routes: RoutesBuilder ) } extension AdminUpdateController { private func render( _ req: Request, editor: UpdateModelEditor ) async -> Response { req.templates.renderHtml( await updateTemplate(req, editor) ) } func updateView( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let editor = UpdateModelEditor( model: model as! UpdateModelEditor.Model, form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) try await editor.read(req: req) return await render(req, editor: editor) } func updateAction( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let editor = UpdateModelEditor( model: model as! UpdateModelEditor.Model, form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) try await editor.process(req: req) let isValid = try await editor.validate(req: req) guard isValid else { return await render(req, editor: editor) } try await editor.write(req: req) try await update(req, editor.model as! DatabaseModel) try await editor.save(req: req) return req.redirect(to: req.url.path) } func updateTemplate( _ req: Request, _ editor: UpdateModelEditor ) async -> TemplateRepresentable { await AdminEditorPageTemplate( updateContext(req, editor) ) } func updateContext( _ req: Request, _ editor: UpdateModelEditor ) async -> AdminEditorPageContext { let path = "delete/?redirect=" + req.url.path.pathComponents.dropLast(2).string + "&cancel=" + req.url.path let model = editor.model as! DatabaseModel return .init( title: "Update", form: editor.form.getContext(req), navigation: updateNavigation(req, model), breadcrumbs: updateBreadcrumbs(req, model), actions: [ LinkContext( label: "Delete", path: path, dropLast: 1 ), ] ) } func updateBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: Self.moduleName.capitalized, dropLast: 3 ), LinkContext( label: Self.modelName.plural.capitalized, dropLast: 2 ), ] } func updateNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: "Details", dropLast: 1 ), ] } func setupUpdateRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.get("update", use: updateView) existingModelRoutes.post("update", use: updateAction) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDashboardContext.swift ================================================ struct AdminDashboardContext { let icon: String let title: String let message: String } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDeletePageContext.swift ================================================ public struct AdminDeletePageContext { public let title: String public let name: String public let type: String public let form: FormContext public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public init( title: String, name: String, type: String, form: FormContext, navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [] ) { self.title = title self.name = name self.type = type self.form = form self.navigation = navigation self.breadcrumbs = breadcrumbs } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDetailPageContext.swift ================================================ public struct AdminDetailPageContext { public let title: String public let fields: [DetailContext] public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public let actions: [LinkContext] public init( title: String, fields: [DetailContext], navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [], actions: [LinkContext] = [] ) { self.title = title self.fields = fields self.navigation = navigation self.breadcrumbs = breadcrumbs self.actions = actions } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminEditorPageContext.swift ================================================ public struct AdminEditorPageContext { public let title: String public let form: FormContext public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public let actions: [LinkContext] public init( title: String, form: FormContext, navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [], actions: [LinkContext] = [] ) { self.title = title self.form = form self.navigation = navigation self.breadcrumbs = breadcrumbs self.actions = actions } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminIndexContext.swift ================================================ public struct AdminIndexContext { public let title: String public let breadcrumbs: [LinkContext] public init( title: String, breadcrumbs: [LinkContext] = [] ) { self.title = title self.breadcrumbs = breadcrumbs } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminListPageContext.swift ================================================ public struct AdminListPageContext { public let title: String public let table: TableContext public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public init( title: String, table: TableContext, navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [] ) { self.title = title self.table = table self.navigation = navigation self.breadcrumbs = breadcrumbs } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDashboardTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminDashboardTemplate: TemplateRepresentable { var context: AdminDashboardContext init( _ context: AdminDashboardContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } Div { let widgets: [TemplateRepresentable] = req.invokeAll("admin-widget") widgets.map { $0.render(req) } } .class("widgets") } .id("dashboard") .class("container") } .render(req) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDeletePageTemplate.swift ================================================ import Vapor import SwiftHtml public struct AdminDeletePageTemplate: TemplateRepresentable { var context: AdminDeletePageContext public init( _ context: AdminDeletePageContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { Span("🗑") .class("icon") H1(context.title) P("You are about to permanently delete the
`\(context.name)` \(context.type).") FormTemplate(context.form).render(req) A("Cancel") .href((try? req.query.get(String.self, at: "cancel")) ?? "#") .class(["button", "cancel"]) } .class(["lead", "container", "center"]) } .render(req) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDetailPageTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminDetailPageTemplate: TemplateRepresentable { var context: AdminDetailPageContext init( _ context: AdminDetailPageContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { Div { H1(context.title) for item in context.navigation { LinkTemplate(item).render(req) } } .class("lead") Dl { for item in context.fields { DetailTemplate(item).render(req) } } Section { for item in context.actions { LinkTemplate(item).render(req) } } } .class("container") } .render(req) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Admin/Templates/Html/AdminEditorPageTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminEditorPageTemplate: TemplateRepresentable { var context: AdminEditorPageContext init( _ context: AdminEditorPageContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { Div { H1(context.title) for item in context.navigation { LinkTemplate(item).render(req) } } .class("lead") FormTemplate(context.form).render(req) Section { for item in context.actions { LinkTemplate(item).render(req) } } } .class("container") } .render(req) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Admin/Templates/Html/AdminIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg public struct AdminIndexTemplate: TemplateRepresentable { public var context: AdminIndexContext var body: Tag public init( _ context: AdminIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Meta() .name("robots") .content("noindex") Link(rel: .shortcutIcon) .href("/images/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css") Link(rel: .stylesheet) .href("/css/admin.css") Title(context.title) } Body { Div { A { Img(src: "/img/logo.png", alt: "Logo") .title("Logo") .style("width: 300px") } .href("/") Nav { Input() .type(.checkbox) .id("secondary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("secondary-menu-button") Div { A("Sign out") .href("/sign-out/") } .class("menu-items") } .id("secondary-menu") } .id("navigation") Div { Nav { A("Admin") .href("/admin/") for breadcrumb in context.breadcrumbs { LinkTemplate(breadcrumb).render(req) } } } .class("breadcrumb") Main { body } Script() .type(.javascript) .src("/js/admin.js") } } .lang("en-US") } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Admin/Templates/Html/AdminListPageTemplate.swift ================================================ import Vapor import SwiftHtml public struct AdminListPageTemplate: TemplateRepresentable { var context: AdminListPageContext public init( _ context: AdminListPageContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { H1(context.title) P { context.navigation.map { LinkTemplate($0).render(req) } } } .class("lead") if context.table.rows.isEmpty { Div { Span("🔍") .class("icon") H2("Oh no") P("This list is empty right now.") A("Try again →") .href(req.url.path) .class("button-1") } .class(["lead", "container", "center"]) } else { TableTemplate(context.table).render(req) } } .render(req) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Api/ApiModule.swift ================================================ import Vapor struct ApiModule: ModuleInterface { let router = ApiRouter() func boot(_ app: Application) throws { app.middleware.use(ApiErrorMiddleware()) try router.boot(routes: app.routes) } func setUp(_ app: Application) throws { try router.setUpRoutesHooks(app: app) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Api/ApiRouter.swift ================================================ import Vapor struct ApiRouter: RouteCollection { func boot(routes: RoutesBuilder) throws {} func setUpRoutesHooks(app: Application) throws { let apiRoutes = app.routes .grouped(AuthenticatedUser.guardMiddleware()) .grouped("api") let _: [Void] = app.invokeAll( "api-routes", args: ["routes": apiRoutes] ) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Api/Controllers/ApiController.swift ================================================ import Vapor protocol ApiController: ApiListController, ApiDetailController, ApiCreateController, ApiUpdateController, ApiPatchController, ApiDeleteController { func validators( optional: Bool ) -> [AsyncValidator] func setupRoutes( _ routes: RoutesBuilder ) } extension ApiController { func validators( optional: Bool ) -> [AsyncValidator] { [] } func createValidators() -> [AsyncValidator] { validators(optional: false) } func updateValidators() -> [AsyncValidator] { validators(optional: false) } func patchValidators() -> [AsyncValidator] { validators(optional: true) } func createResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response { try await detailOutput(req, model) .encodeResponse(status: .created, for: req) } func updateResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response { try await detailOutput(req, model) .encodeResponse(for: req) } func patchResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response { try await detailOutput(req, model) .encodeResponse(for: req) } func setupRoutes( _ routes: RoutesBuilder ) { setupListRoutes(routes) setupDetailRoutes(routes) setupCreateRoutes(routes) setupUpdateRoutes(routes) setupPatchRoutes(routes) setupDeleteRoutes(routes) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Api/Controllers/ApiCreateController.swift ================================================ import Vapor protocol ApiCreateController: CreateController { associatedtype CreateObject: Decodable func createValidators() -> [AsyncValidator] func createInput( _ req: Request, _ model: DatabaseModel, _ input: CreateObject ) async throws func createApi( _ req: Request ) async throws -> Response func createResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response func setupCreateRoutes( _ routes: RoutesBuilder ) } extension ApiCreateController { func createValidators() -> [AsyncValidator] { [] } func createApi( _ req: Request ) async throws -> Response { try await RequestValidator(createValidators()).validate(req) let input = try req.content.decode(CreateObject.self) let model = DatabaseModel() try await createInput(req, model, input) try await create(req, model) return try await createResponse(req, model) } func setupCreateRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) baseRoutes.post(use: createApi) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Api/Controllers/ApiDeleteController.swift ================================================ import Vapor public protocol ApiDeleteController: DeleteController { func deleteApi( _ req: Request ) async throws -> HTTPStatus func setupDeleteRoutes( _ routes: RoutesBuilder ) } public extension ApiDeleteController { func deleteApi( _ req: Request ) async throws -> HTTPStatus { let model = try await findBy(identifier(req), on: req.db) try await delete(req, model) return .noContent } func setupDeleteRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.delete(use: deleteApi) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Api/Controllers/ApiDetailController.swift ================================================ import Vapor protocol ApiDetailController: DetailController { associatedtype DetailObject: Content func detailOutput( _ req: Request, _ model: DatabaseModel ) async throws -> DetailObject func detailApi( _ req: Request ) async throws -> DetailObject func setupDetailRoutes( _ routes: RoutesBuilder ) } extension ApiDetailController { func detailApi( _ req: Request ) async throws -> DetailObject { let model = try await detail(req) return try await detailOutput(req, model) } func setupDetailRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.get(use: detailApi) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Api/Controllers/ApiListController.swift ================================================ import Vapor protocol ApiListController: ListController { associatedtype ListObject: Content func listOutput( _ req: Request, _ models: [DatabaseModel] ) async throws -> [ListObject] func listApi( _ req: Request ) async throws -> [ListObject] func setupListRoutes( _ routes: RoutesBuilder ) } extension ApiListController { func listApi( _ req: Request ) async throws -> [ListObject] { let models = try await list(req) return try await listOutput(req, models) } func setupListRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) baseRoutes.get(use: listApi) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Api/Controllers/ApiPatchController.swift ================================================ import Vapor public protocol ApiPatchController: PatchController { associatedtype PatchObject: Decodable func patchValidators() -> [AsyncValidator] func patchInput( _ req: Request, _ model: DatabaseModel, _ input: PatchObject ) async throws func patchApi( _ req: Request ) async throws -> Response func patchResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response func setupPatchRoutes( _ routes: RoutesBuilder ) } public extension ApiPatchController { func patchValidators() -> [AsyncValidator] { [] } func patchApi( _ req: Request ) async throws -> Response { try await RequestValidator(patchValidators()).validate(req) let model = try await findBy(identifier(req), on: req.db) let input = try req.content.decode(PatchObject.self) try await patchInput(req, model, input) try await patch(req, model) return try await patchResponse(req, model) } func setupPatchRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.patch(use: patchApi) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Api/Controllers/ApiUpdateController.swift ================================================ import Vapor public protocol ApiUpdateController: UpdateController { associatedtype UpdateObject: Decodable func updateValidators() -> [AsyncValidator] func updateInput( _ req: Request, _ model: DatabaseModel, _ input: UpdateObject ) async throws func updateApi( _ req: Request ) async throws -> Response func updateResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response func setupUpdateRoutes( _ routes: RoutesBuilder ) } public extension ApiUpdateController { func updateValidators() -> [AsyncValidator] { [] } func updateApi( _ req: Request ) async throws -> Response { try await RequestValidator(updateValidators()).validate(req) let model = try await findBy(identifier(req), on: req.db) let input = try req.content.decode(UpdateObject.self) try await updateInput(req, model, input) try await update(req, model) return try await updateResponse(req, model) } func setupUpdateRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.put(use: updateApi) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Api/Middlewares/ApiErrorMiddleware.swift ================================================ import Vapor struct ApiErrorMiddleware: AsyncMiddleware { func respond( to req: Request, chainingTo next: AsyncResponder ) async throws -> Response { do { return try await next.respond(to: req) } catch { let status: HTTPResponseStatus let headers: HTTPHeaders let message: String? let details: [ValidationErrorDetail] switch error { case let abort as ValidationAbort: status = abort.abort.status headers = abort.abort.headers message = abort.message details = abort.details case let abort as Abort: status = abort.status headers = abort.headers message = abort.reason details = [] default: status = .internalServerError headers = [:] if req.application.environment.isRelease { message = "Something went wrong." } else { message = error.localizedDescription } details = [] } req.logger.report(error: error) let response = Response( status: status, headers: headers ) do { let encoder = JSONEncoder() let data = try encoder.encode( ValidationError( message: message, details: details ) ) response.body = .init(data: data) response.headers.replaceOrAdd( name: .contentType, value: "application/json; charset=utf-8" ) } catch { response.body = .init(string: "Oops: \(error)") response.headers.replaceOrAdd( name: .contentType, value: "text/plain; charset=utf-8" ) } return response } } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/BlogModule.swift ================================================ import Vapor struct BlogModule: ModuleInterface { let router = BlogRouter() func boot(_ app: Application) throws { app.migrations.add(BlogMigrations.v1()) app.migrations.add(BlogMigrations.seed()) app.hooks.register("admin-widget", use: adminWidgetHook) app.hooks.register("admin-routes", use: router.adminRoutesHook) app.hooks.register("api-routes", use: router.apiRoutesHook) app.hooks.registerAsync("response", use: router.responseHook) try router.boot(routes: app.routes) } func adminWidgetHook( _ args: HookArguments ) -> TemplateRepresentable { BlogAdminWidgetTemplate() } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/BlogRouter.swift ================================================ import Vapor struct BlogRouter: RouteCollection { let frontendController = BlogFrontendController() let postAdminController = BlogPostAdminController() let postApiController = BlogPostApiController() let categoryAdminController = BlogCategoryAdminController() let categoryApiController = BlogCategoryApiController() func boot(routes: RoutesBuilder) throws { routes.get("blog", use: frontendController.blogView) } func adminRoutesHook(_ args: HookArguments) -> Void { let routes = args["routes"] as! RoutesBuilder postAdminController.setupRoutes(routes) categoryAdminController.setupRoutes(routes) } func apiRoutesHook(_ args: HookArguments) -> Void { let routes = args["routes"] as! RoutesBuilder postApiController.setupRoutes(routes) categoryApiController.setupRoutes(routes) } func responseHook( _ args: HookArguments ) async throws -> Response? { let req = args["req"] as! Request return try await frontendController.postView(req) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Controllers/BlogCategoryAdminController.swift ================================================ import Vapor import Fluent struct BlogCategoryAdminController: AdminController { typealias ApiModel = Blog.Category typealias DatabaseModel = BlogCategoryModel typealias CreateModelEditor = BlogCategoryEditor typealias UpdateModelEditor = BlogCategoryEditor func listColumns() -> [ColumnContext] { [ .init("title"), ] } func listCells( for model: DatabaseModel ) -> [CellContext] { [ .init( model.title, link: .init(label: model.title) ), ] } func detailFields( for model: DatabaseModel ) -> [DetailContext] { [ .init("title", model.title), ] } func deleteInfo( _ model: DatabaseModel ) -> String { model.title } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Controllers/BlogCategoryApiController.swift ================================================ import Vapor extension Blog.Category.List: Content {} extension Blog.Category.Detail: Content {} struct BlogCategoryApiController: ApiController { typealias ApiModel = Blog.Category typealias DatabaseModel = BlogCategoryModel @AsyncValidatorBuilder func validators( optional: Bool ) -> [AsyncValidator] { KeyedContentValidator.required("title", optional: optional) } func listOutput( _ req: Request, _ models: [BlogCategoryModel] ) async throws -> [Blog.Category.List] { models.map { .init(id: $0.id!, title: $0.title) } } func detailOutput( _ req: Request, _ model: BlogCategoryModel ) async throws -> Blog.Category.Detail { .init( id: model.id!, title: model.title ) } func createInput( _ req: Request, _ model: BlogCategoryModel, _ input: Blog.Category.Create ) async throws { model.title = input.title } func updateInput( _ req: Request, _ model: BlogCategoryModel, _ input: Blog.Category.Update ) async throws { model.title = input.title } func patchInput( _ req: Request, _ model: BlogCategoryModel, _ input: Blog.Category.Patch ) async throws { model.title = input.title ?? model.title } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift ================================================ import Vapor import Fluent struct BlogFrontendController { func blogView(req: Request) async throws -> Response { let posts = try await BlogPostModel .query(on: req.db) .sort(\.$date, .descending) .all() let api = BlogPostApiController() let listOutput = try await api.listOutput(req, posts) let ctx = BlogPostsContext( icon: "🔥", title: "Blog", message: "Hot news and stories about everything.", posts: listOutput ) return req.templates.renderHtml(BlogPostsTemplate(ctx)) } func postView( _ req: Request ) async throws -> Response? { let slug = req.url.path.trimmingCharacters( in: .init(charactersIn: "/") ) guard let post = try await BlogPostModel .query(on: req.db) .filter(\.$slug == slug) .first() else { return nil } let model = try await BlogPostApiController().detailOutput(req, post) let context = BlogPostContext(post: model) return req.templates.renderHtml(BlogPostTemplate(context)) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Controllers/BlogPostAdminController.swift ================================================ import Vapor import Fluent struct BlogPostAdminController: AdminController { typealias ApiModel = Blog.Post typealias DatabaseModel = BlogPostModel typealias CreateModelEditor = BlogPostEditor typealias UpdateModelEditor = BlogPostEditor func listColumns() -> [ColumnContext] { [ .init("image"), .init("title"), ] } func listCells(for model: DatabaseModel) -> [CellContext] { [ .init(model.imageKey, type: .image), .init(model.title, link: .init(label: model.title)), ] } func detailFields(for model: DatabaseModel) -> [DetailContext] { [ .init("image", model.imageKey, type: .image), .init("title", model.title), ] } func deleteInfo( _ model: DatabaseModel ) -> String { model.title } func beforeDelete( _ req: Request, _ model: BlogPostModel ) async throws { try await req.fs.delete(key: model.imageKey) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Controllers/BlogPostApiController.swift ================================================ import Vapor extension Blog.Post.List: Content {} extension Blog.Post.Detail: Content {} struct BlogPostApiController: ApiController { typealias ApiModel = Blog.Post typealias DatabaseModel = BlogPostModel @AsyncValidatorBuilder func validators(optional: Bool) -> [AsyncValidator] { KeyedContentValidator.required("title", optional: optional) KeyedContentValidator.required("slug", optional: optional) KeyedContentValidator.required("image", optional: optional) KeyedContentValidator.required("excerpt", optional: optional) KeyedContentValidator.required("content", optional: optional) KeyedContentValidator.required("categoryId", optional: optional) KeyedContentValidator("categoryId", "Invalid or missing category", optional: optional) { value, req in try await BlogCategoryModel.find(value, on: req.db) != nil } } func listOutput( _ req: Request, _ models: [DatabaseModel] ) async throws -> [Blog.Post.List] { models.map { model in .init( id: model.id!, title: model.title, slug: model.slug, image: model.imageKey, excerpt: model.excerpt, date: model.date ) } } func detailOutput( _ req: Request, _ model: DatabaseModel ) async throws -> Blog.Post.Detail { guard let categoryModel = try await BlogCategoryModel .find(model.$category.id, on: req.db), let category = try await BlogCategoryApiController() .listOutput(req, [categoryModel]) .first else { throw Abort(.internalServerError) } return .init( id: model.id!, title: model.title, slug: model.slug, image: model.imageKey, excerpt: model.excerpt, date: model.date, category: category, content: model.content ) } func createInput( _ req: Request, _ model: DatabaseModel, _ input: Blog.Post.Create ) async throws { model.title = input.title model.slug = input.slug model.imageKey = input.image model.excerpt = input.excerpt model.date = input.date model.content = input.content model.$category.id = input.categoryId } func updateInput( _ req: Request, _ model: DatabaseModel, _ input: Blog.Post.Update ) async throws { model.title = input.title model.slug = input.slug model.imageKey = input.image model.excerpt = input.excerpt model.date = input.date model.content = input.content model.$category.id = input.categoryId } func patchInput( _ req: Request, _ model: DatabaseModel, _ input: Blog.Post.Patch ) async throws { model.title = input.title ?? model.title model.slug = input.slug ?? model.slug model.imageKey = input.image ?? model.imageKey model.excerpt = input.excerpt ?? model.excerpt model.date = input.date ?? model.date model.content = input.content ?? model.content model.$category.id = input.categoryId ?? model.$category.id } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Database/Migrations/BlogMigrations.swift ================================================ import Foundation import Fluent enum BlogMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema) .id() .field(BlogCategoryModel.FieldKeys.v1.title, .string, .required) .create() try await db.schema(BlogPostModel.schema) .id() .field(BlogPostModel.FieldKeys.v1.title, .string, .required) .field(BlogPostModel.FieldKeys.v1.slug, .string, .required) .field(BlogPostModel.FieldKeys.v1.imageKey, .string, .required) .field(BlogPostModel.FieldKeys.v1.excerpt, .data, .required) .field(BlogPostModel.FieldKeys.v1.date, .datetime, .required) .field(BlogPostModel.FieldKeys.v1.content, .data, .required) .field(BlogPostModel.FieldKeys.v1.categoryId, .uuid) .foreignKey( BlogPostModel.FieldKeys.v1.categoryId, references: BlogCategoryModel.schema, .id, onDelete: DatabaseSchema.ForeignKeyAction.setNull, onUpdate: .cascade ) .unique(on: BlogPostModel.FieldKeys.v1.slug) .create() } func revert(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema).delete() try await db.schema(BlogPostModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let categories = (1...4).map { index in BlogCategoryModel(title: "Sample category #\(index)") } try await categories.create(on: db) try await (1...9).map { index in BlogPostModel( id: nil, title: "Sample post #\(index)", slug: "sample-post-\(index)", imageKey: "/img/posts/\(String(format: "%02d", index + 1)).jpg", excerpt: "Lorem ipsum", date: Date().addingTimeInterval(-Double.random(in: 0...(86400 * 60))), content: "Lorem ipsum dolor sit amet.", categoryId: categories[Int.random(in: 0.. [FormComponent] { ImageField("image", path: "blog/post") .read { [unowned self] in $1.output.context.previewUrl = model.imageKey ($1 as! ImageField).imageKey = model.imageKey } .write { [unowned self] in model.imageKey = ($1 as! ImageField).imageKey ?? "" } InputField("slug") .config { $0.output.context.label.required = true } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = model.slug } .write { [unowned self] in model.slug = $1.input } InputField("title") .config { $0.output.context.label.required = true } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = model.title } .write { [unowned self] in model.title = $1.input } InputField("date") .config { $0.output.context.label.required = true $0.output.context.value = dateFormatter.string(from: Date()) } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = dateFormatter.string(from: model.date) } .write { [unowned self] in model.date = dateFormatter.date(from: $1.input) ?? Date() } TextareaField("excerpt") .read { [unowned self] in $1.output.context.value = model.excerpt } .write { [unowned self] in model.excerpt = $1.input } TextareaField("content") .read { [unowned self] in $1.output.context.value = model.content } .write { [unowned self] in model.content = $1.input } SelectField("category") .load { req, field in let categories = try await BlogCategoryModel .query(on: req.db) .all() field.output.context.options = categories.map { OptionContext(key: $0.id!.uuidString, label: $0.title) } } .read { [unowned self] req, field in field.output.context.value = model.$category.id.uuidString } .write { [unowned self] req, field in if let uuid = UUID(uuidString: field.input), let category = try await BlogCategoryModel .find(uuid, on: req.db) { model.$category.id = category.id! } } } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Objects/Blog.swift ================================================ enum Blog: ApiModuleInterface { enum Post: ApiModelInterface { typealias Module = Blog } enum Category: ApiModelInterface { typealias Module = Blog static let pathKey: String = "categories" } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Objects/BlogCategory.swift ================================================ import Foundation extension Blog.Category { struct List: Codable { let id: UUID let title: String } struct Detail: Codable { let id: UUID let title: String } struct Create: Codable { let title: String } struct Update: Codable { let title: String } struct Patch: Codable { let title: String? } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Objects/BlogPost.swift ================================================ import Foundation extension Blog.Post { struct List: Codable { let id: UUID let title: String let slug: String let image: String let excerpt: String let date: Date } struct Detail: Codable { let id: UUID let title: String let slug: String let image: String let excerpt: String let date: Date let category: Blog.Category.List let content: String } struct Create: Codable { let title: String let slug: String let image: String let excerpt: String let date: Date let content: String let categoryId: UUID } struct Update: Codable { let title: String let slug: String let image: String let excerpt: String let date: Date let content: String let categoryId: UUID } struct Patch: Codable { let title: String? let slug: String? let image: String? let excerpt: String? let date: Date? let content: String? let categoryId: UUID? } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminDeleteContext.swift ================================================ struct BlogPostAdminDeleteContext { let title: String let name: String let type: String } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminDetailContext.swift ================================================ struct BlogPostAdminDetailContext { let title: String let detail: Blog.Post.Detail } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminEditContext.swift ================================================ struct BlogPostAdminEditContext { let title: String let form: TemplateRepresentable } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminListContext.swift ================================================ struct BlogPostAdminListContext { let title: String let list: [Blog.Post.List] } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostContext.swift ================================================ struct BlogPostContext { let post: Blog.Post.Detail } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostsContext.swift ================================================ struct BlogPostsContext { let icon: String let title: String let message: String let posts: [Blog.Post.List] } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Templates/Html/BlogAdminWidgetTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogAdminWidgetTemplate: TemplateRepresentable { @TagBuilder func render(_ req: Request) -> Tag { H2("Blog") Ul { Li { A("Posts") .href("/admin/blog/posts/") } Li { A("Categories") .href("/admin/blog/categories/") } } } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminDeleteTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminDeleteTemplate: TemplateRepresentable { var context: BlogPostAdminDeleteContext init( _ context: BlogPostAdminDeleteContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Span("🗑") .class("icon") H1(context.title) P("You are about to permanently delete the
`\(context.name)` \(context.type).") Form { Input() .type(.submit) .class(["button", "destructive"]) .style("display: inline") .value("Delete") A("Cancel") .href("/admin/blog/posts/") .class(["button", "cancel"]) } .method(.post) .id("delete-form") } .class(["lead", "container", "center"]) } .render(req) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminDetailTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminDetailTemplate: TemplateRepresentable { var context: BlogPostAdminDetailContext init( _ context: BlogPostAdminDetailContext ) { self.context = context } var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .short return formatter }() @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") Dl { Dt("Image") Dd { Img( src: context.detail.image, alt: context.detail.title ) } Dt("Title") Dd(context.detail.title) Dt("Excerpt") Dd(context.detail.excerpt) Dt("Date") Dd(dateFormatter.string(from: context.detail.date)) Dt("Content") Dd(context.detail.content) } } .id("detail") .class("container") } .render(req) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminEditTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminEditTemplate: TemplateRepresentable { var context: BlogPostAdminEditContext init( _ context: BlogPostAdminEditContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") context.form.render(req) } .id("edit") .class("container") } .render(req) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminListTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminListTemplate: TemplateRepresentable { var context: BlogPostAdminListContext init( _ context: BlogPostAdminListContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") Table { Thead { Tr { Th("Image") Th("Title") Th("Preview") } } Tbody { for item in context.list { Tr { Td { Img(src: item.image, alt: item.title) } Td { A(item.title) .href("/admin/blog/posts/" + item.id.uuidString + "/") } Td { A("Preview") .href("/" + item.slug + "/") } } } } } } .id("list") } .render(req) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostTemplate: TemplateRepresentable { var context: BlogPostContext init( _ context: BlogPostContext ) { self.context = context } var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .short return formatter }() @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.post.title) ) { Div { Section { P(dateFormatter.string(from: context.post.date)) H1(context.post.title) P(context.post.excerpt) } .class(["lead", "container"]) Img(src: context.post.image, alt: context.post.title) Article { Text(context.post.content) } .class("container") } .id("post") } .render(req) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostsTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostsTemplate: TemplateRepresentable { var context: BlogPostsContext init( _ context: BlogPostsContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") Div { for post in context.posts { Article { A { Img(src: post.image, alt: post.title) H2(post.title) P(post.excerpt) } .href("/\(post.slug)/") } } } .class("grid-221") } .id("blog") } .render(req) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/User/Authenticators/UserCredentialsAuthenticator.swift ================================================ import Vapor import Fluent struct UserCredentialsAuthenticator: AsyncCredentialsAuthenticator { struct Credentials: Content { let email: String let password: String } func authenticate( credentials: Credentials, for req: Request ) async throws { guard let user = try await UserAccountModel .query(on: req.db) .filter(\.$email == credentials.email) .first() else { return } do { guard try Bcrypt.verify( credentials.password, created: user.password ) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } catch { // do nothing... } } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/User/Authenticators/UserSessionAuthenticator.swift ================================================ import Vapor import Fluent struct UserSessionAuthenticator: AsyncSessionAuthenticator { typealias User = AuthenticatedUser func authenticate( sessionID: User.SessionID, for req: Request ) async throws { guard let user = try await UserAccountModel .find(sessionID, on: req.db) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/User/Authenticators/UserTokenAuthenticator.swift ================================================ import Vapor import Fluent struct UserTokenAuthenticator: AsyncBearerAuthenticator { func authenticate( bearer: BearerAuthorization, for req: Request ) async throws { guard let token = try await UserTokenModel .query(on: req.db) .filter(\.$value == bearer.token) .first() else { return } guard let user = try await UserAccountModel .find(token.$user.id, on: req.db) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/User/Controllers/UserApiController.swift ================================================ import Vapor extension User.Token.Detail: Content {} struct UserApiController { func signInApi( req: Request ) async throws -> User.Token.Detail { guard let user = req.auth.get(AuthenticatedUser.self) else { throw Abort(.unauthorized) } let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789=" let tokenValue = String((0..<64).map { _ in letters.randomElement()! }) let token = UserTokenModel( value: tokenValue, userId: user.id ) try await token.create(on: req.db) let account = User.Account.Detail( id: user.id, email: user.email ) return .init( id: token.id!, value: token.value, user: account ) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/User/Controllers/UserFrontendController.swift ================================================ import Vapor struct UserFrontendController { private func renderSignInView( _ req: Request, _ form: UserLoginForm ) -> Response { let template = UserLoginTemplate( .init( icon: "⬇️", title: "Sign in", message: "Please log in with your existing account", form: form.render(req: req) ) ) return req.templates.renderHtml(template) } func signInView( _ req: Request ) async throws -> Response { renderSignInView(req, .init()) } func signInAction( _ req: Request ) async throws -> Response { /// the user is authenticated, we can store the user data inside the session too if let user = req.auth.get(AuthenticatedUser.self) { req.session.authenticate(user) return req.redirect(to: "/") } let form = UserLoginForm() try await form.process(req: req) let isValid = try await form.validate(req: req) if !isValid { form.error = "Invalid email or password." } return renderSignInView(req, form) } func signOut( _ req: Request ) throws -> Response { req.auth.logout(AuthenticatedUser.self) req.session.unauthenticate(AuthenticatedUser.self) return req.redirect(to: "/") } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/User/Database/Migrations/UserMigrations.swift ================================================ import Vapor import Fluent enum UserMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(UserAccountModel.schema) .id() .field(UserAccountModel.FieldKeys.v1.email, .string, .required) .field(UserAccountModel.FieldKeys.v1.password, .string, .required) .unique(on: UserAccountModel.FieldKeys.v1.email) .create() try await db.schema(UserTokenModel.schema) .id() .field(UserTokenModel.FieldKeys.v1.value, .string, .required) .field(UserTokenModel.FieldKeys.v1.userId, .uuid, .required) .foreignKey( UserTokenModel.FieldKeys.v1.userId, references: UserAccountModel.schema, .id ) .unique(on: UserTokenModel.FieldKeys.v1.value) .create() } func revert(on db: Database) async throws { try await db.schema(UserTokenModel.schema).delete() try await db.schema(UserAccountModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let email = "root@localhost.com" let password = "ChangeMe1" let user = UserAccountModel( email: email, password: try Bcrypt.hash(password) ) try await user.create(on: db) } func revert(on db: Database) async throws { try await UserAccountModel.query(on: db).delete() } } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/User/Database/Models/UserAccountModel.swift ================================================ import Vapor import Fluent final class UserAccountModel: DatabaseModelInterface { typealias Module = UserModule struct FieldKeys { struct v1 { static var email: FieldKey { "email" } static var password: FieldKey { "password" } } } @ID() var id: UUID? @Field(key: FieldKeys.v1.email) var email: String @Field(key: FieldKeys.v1.password) var password: String init() { } init( id: UUID? = nil, email: String, password: String ) { self.id = id self.email = email self.password = password } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/User/Database/Models/UserTokenModel.swift ================================================ import Vapor import Fluent final class UserTokenModel: DatabaseModelInterface { typealias Module = UserModule struct FieldKeys { struct v1 { static var value: FieldKey { "value" } static var userId: FieldKey { "user_id" } } } @ID() var id: UUID? @Field(key: FieldKeys.v1.value) var value: String @Parent(key: FieldKeys.v1.userId) var user: UserAccountModel init() { } init( id: UUID? = nil, value: String, userId: UUID ) { self.id = id self.value = value self.$user.id = userId } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/User/Forms/UserLoginForm.swift ================================================ import Vapor final class UserLoginForm: AbstractForm { public convenience init() { self.init( action: .init( method: .post, url: "/sign-in/" ), submit: "Sign in" ) self.fields = createFields() } @FormComponentBuilder func createFields() -> [FormComponent] { InputField("email") .config { $0.output.context.label.required = true $0.output.context.type = .email } .validators { FormFieldValidator.required($1) FormFieldValidator.email($1) } InputField("password") .config { $0.output.context.label.required = true $0.output.context.type = .password } .validators { FormFieldValidator.required($1) } } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/User/Objects/User.swift ================================================ enum User: ApiModuleInterface { enum Account: ApiModelInterface { typealias Module = User } enum Token: ApiModelInterface { typealias Module = User } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/User/Objects/UserAccount.swift ================================================ import Foundation extension User.Account { struct List: Codable { let id: UUID let email: String } struct Detail: Codable { let id: UUID let email: String } struct Create: Codable { let email: String let password: String } struct Update: Codable { let email: String let password: String? } struct Patch: Codable { let email: String? let password: String? } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/User/Objects/UserToken.swift ================================================ import Foundation extension User.Token { struct Detail: Codable { let id: UUID let value: String let user: User.Account.Detail } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/User/Templates/Contexts/UserLoginContext.swift ================================================ struct UserLoginContext { let icon: String let title: String let message: String let form: TemplateRepresentable init( icon: String, title: String, message: String, form: TemplateRepresentable ) { self.icon = icon self.title = title self.message = message self.form = form } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/User/Templates/Html/UserLoginTemplate.swift ================================================ import Vapor import SwiftHtml struct UserLoginTemplate: TemplateRepresentable { var context: UserLoginContext init( _ context: UserLoginContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") context.form.render(req) } .id("user-login") .class("container") } .render(req) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/User/UserModule.swift ================================================ import Vapor struct UserModule: ModuleInterface { let router = UserRouter() func boot(_ app: Application) throws { app.migrations.add(UserMigrations.v1()) app.migrations.add(UserMigrations.seed()) app.middleware.use(UserSessionAuthenticator()) app.middleware.use(UserTokenAuthenticator()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/User/UserRouter.swift ================================================ import Vapor struct UserRouter: RouteCollection { let frontendController = UserFrontendController() let apiController = UserApiController() func boot(routes: RoutesBuilder) throws { routes.get("sign-in", use: frontendController.signInView) routes .grouped(UserCredentialsAuthenticator()) .post("sign-in", use: frontendController.signInAction) routes.get("sign-out", use: frontendController.signOut) routes.grouped("api") .grouped(UserCredentialsAuthenticator()) .post("sign-in", use: apiController.signInApi) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Web/Controllers/WebFrontendController.swift ================================================ import Vapor struct WebFrontendController { func anyResponse( _ req: Request ) async throws -> Response { let result: [Response?] = try await req.invokeAllAsync("response") guard let response = result.compactMap({ $0 }).first else { throw Abort(.notFound) } return response } func homeView(req: Request) throws -> Response { let ctx = WebHomeContext( icon: "👋", title: "Home", message: "Hi there, welcome to my page.", paragraphs: [ "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.", "Nisi ut aliquip ex ea commodo consequat.", ], link: .init( label: "Read my blog →", url: "/blog/" ) ) return req.templates.renderHtml( WebHomeTemplate(ctx) ) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Web/Templates/Contexts/WebHomeContext.swift ================================================ struct WebHomeContext { let icon: String let title: String let message: String let paragraphs: [String] let link: WebLinkContext } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Web/Templates/Contexts/WebIndexContext.swift ================================================ public struct WebIndexContext { public let title: String public init( title: String ) { self.title = title } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Web/Templates/Contexts/WebLinkContext.swift ================================================ public struct WebLinkContext { public let label: String public let url: String public init( label: String, url: String ) { self.label = label self.url = url } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Web/Templates/Html/WebHomeTemplate.swift ================================================ import Vapor import SwiftHtml struct WebHomeTemplate: TemplateRepresentable { var context: WebHomeContext init( _ context: WebHomeContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") for paragraph in context.paragraphs { P(paragraph) } WebLinkTemplate(context.link).render(req) } .id("home") .class("container") } .render(req) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg public struct WebIndexTemplate: TemplateRepresentable { public var context: WebIndexContext var body: Tag public init( _ context: WebIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Link(rel: .shortcutIcon) .href("/images/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css") Link(rel: .stylesheet) .href("/css/web.css") Title(context.title) } Body { Header { Div { A { Img(src: "/img/logo.png", alt: "Logo") } .id("site-logo") .href("/") Nav { Input() .type(.checkbox) .id("primary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("primary-menu-button") Div { A("Home") .href("/") .class("selected", req.url.path == "/") A("Blog") .href("/blog/") .class("selected", req.url.path == "/blog/") A("About") .href("#") .onClick("javascript:about();") if req.auth.has(AuthenticatedUser.self) { A("Admin") .href("/admin/") A("Sign out") .href("/sign-out/") } else { A("Sign in") .href("/sign-in/") } } .class("menu-items") } .id("primary-menu") } .id("navigation") } Main { body } Footer { Section { P { Text("This site is powered by ") A("Swift") .href("https://swift.org") .target(.blank) Text(" & ") A("Vapor") .href("https://vapor.codes") .target(.blank) Text(".") } P("myPage © 2020-2022") } } Script() .type(.javascript) .src("/js/web.js") } } .lang("en-US") } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Web/Templates/Html/WebLinkTemplate.swift ================================================ import Vapor import SwiftHtml struct WebLinkTemplate: TemplateRepresentable { var context: WebLinkContext init( _ context: WebLinkContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { A(context.label) .href(context.url) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Web/WebModule.swift ================================================ import Vapor struct WebModule: ModuleInterface { let router = WebRouter() func boot(_ app: Application) throws { try router.boot(routes: app.routes) } func setUp(_ app: Application) throws { try router.setUpRoutesHooks(app: app) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Modules/Web/WebRouter.swift ================================================ import Vapor struct WebRouter: RouteCollection { let controller = WebFrontendController() func boot(routes: RoutesBuilder) throws { routes.get(use: controller.homeView) } func setUpRoutesHooks(app: Application) throws { app.routes.get(.anything, use: controller.anyResponse) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Template/Request+Template.swift ================================================ import Vapor public extension Request { var templates: TemplateRenderer { .init(self) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Template/TemplateRenderer.swift ================================================ import Vapor import SwiftHtml public struct TemplateRenderer { var req: Request init(_ req: Request) { self.req = req } public func renderHtml( _ template: TemplateRepresentable, minify: Bool = false, indent: Int = 4 ) -> Response { let doc = Document(.html) { template.render(req) } let body = DocumentRenderer( minify: minify, indent: indent ) .render(doc) return Response( status: .ok, headers: [ "content-type": "text/html" ], body: .init(string: body) ) } } ================================================ FILE: Chapter 15/myProject/Sources/App/Template/TemplateRepresentable.swift ================================================ /// FILE: Sources/App/Template/TemplateRepresentable.swift import Vapor import SwiftSgml public protocol TemplateRepresentable { @TagBuilder func render(_ req: Request) -> Tag } ================================================ FILE: Chapter 15/myProject/Sources/App/configure.swift ================================================ import Vapor import Fluent import FluentSQLiteDriver import Liquid import LiquidLocalDriver public func configure( _ app: Application ) throws { app.fileStorages.use( .local( publicUrl: "http://localhost:8080", publicPath: app.directory.publicDirectory, workDirectory: "assets" ), as: .local ) app.routes.defaultMaxBodySize = "10mb" let dbPath = app.directory.resourcesDirectory + "db.sqlite" app.databases.use(.sqlite(.file(dbPath)), as: .sqlite) app.middleware.use( FileMiddleware( publicDirectory: app.directory.publicDirectory ) ) app.middleware.use(ExtendPathMiddleware()) app.sessions.use(.fluent) app.migrations.add(SessionRecord.migration) app.middleware.use(app.sessions.middleware) let modules: [ModuleInterface] = [ WebModule(), UserModule(), AdminModule(), ApiModule(), BlogModule(), ] for module in modules { try module.boot(app) } for module in modules { try module.setUp(app) } try app.autoMigrate().wait() } ================================================ FILE: Chapter 15/myProject/Sources/App/routes.swift ================================================ import Vapor import SwiftHtml func routes(_ app: Application) throws { app.routes.get { req -> Response in req.templates.renderHtml( WebIndexTemplate( WebIndexContext( title: "Home" ) ) { P("Hi there, welcome to my page!") } ) } } ================================================ FILE: Chapter 15/myProject/Sources/Run/main.swift ================================================ import App import Vapor var env = try Environment.detect() try LoggingSystem.bootstrap(from: &env) let app = Application(env) defer { app.shutdown() } try configure(app) try app.run() ================================================ FILE: Chapter 15/myProject/Tests/AppTests/AppTests.swift ================================================ @testable import App import XCTVapor final class AppTests: AppTestCase { func testHomePage() throws { let app = try createTestApp() defer { app.shutdown() } try app.testable(method: .inMemory).test(.GET, "") { res in XCTAssertEqual(res.status, .ok) let contentType = try XCTUnwrap(res.headers.contentType) XCTAssertEqual(contentType, .html) XCTAssertTrue(res.body.string.contains("Home")) } } func testAuth() throws { let app = try createTestApp() defer { app.shutdown() } let email = "root@localhost.com" let token = try authenticate( .init( email: email, password: "ChangeMe1" ), app ) XCTAssertEqual(token.user.email, email) } } ================================================ FILE: Chapter 15/myProject/Tests/AppTests/BlogCategoryApiTests.swift ================================================ @testable import App import XCTVapor extension Blog.Category.Create: Content {} final class BlogCategoryApiTests: AppTestCase { func testList() throws { let app = try createTestApp() let token = try authenticateRoot(app) defer { app.shutdown() } let headers = HTTPHeaders([("Authorization", "Bearer \(token.value)")]) try app //.testable(method: .inMemory) .testable(method: .running(port: 8081)) .test(.GET, "/api/blog/categories/", headers: headers) { res in XCTAssertEqual(res.status, .ok) let contentType = try XCTUnwrap(res.headers.contentType) XCTAssertEqual(contentType, .json) XCTAssertContent([Blog.Category.List].self, res) { content in XCTAssertEqual(content.count, 4) } } } func testCreate() throws { let app = try createTestApp() let token = try authenticateRoot(app) defer { app.shutdown() } let headers = HTTPHeaders([("Authorization", "Bearer \(token.value)")]) let newCategory = Blog.Category.Create(title: "Test category") try app.test( .POST, "/api/blog/categories/", headers: headers, content: newCategory ) { res in XCTAssertEqual(res.status, .created) let contentType = try XCTUnwrap(res.headers.contentType) XCTAssertEqual(contentType, .json) XCTAssertContent(Blog.Category.Detail.self, res) { content in XCTAssertEqual(content.title, newCategory.title) } } } func testCreateListUpdate() throws { let app = try createTestApp() let token = try authenticateRoot(app) defer { app.shutdown() } let headers = HTTPHeaders([("Authorization", "Bearer \(token.value)")]) let newCategory = Blog.Category.Create(title: "Test category") try app .test( .POST, "/api/blog/categories/", headers: headers, content: newCategory ) { res in XCTAssertEqual(res.status, .created) let contentType = try XCTUnwrap(res.headers.contentType) XCTAssertEqual(contentType, .json) XCTAssertContent(Blog.Category.Detail.self, res) { content in XCTAssertEqual(content.title, newCategory.title) } } .test( .GET, "/api/blog/categories/", headers: headers ) { res in XCTAssertEqual(res.status, .ok) let contentType = try XCTUnwrap(res.headers.contentType) XCTAssertEqual(contentType, .json) XCTAssertContent([Blog.Category.List].self, res) { content in XCTAssertEqual(content.count, 5) } } } } ================================================ FILE: Chapter 15/myProject/Tests/AppTests/BlogPostApiTests.swift ================================================ @testable import App import XCTVapor import Spec extension Blog.Post.Create: Content {} extension Blog.Post.Update: Content {} final class BlogPostApiTests: AppTestCase { func testList() throws { let app = try createTestApp() let token = try authenticateRoot(app) defer { app.shutdown() } try app .describe("Blog posts list API should be fine") .get("/api/blog/posts/") .bearerToken(token.value) .expect(.ok) .expect(.json) .expect([Blog.Post.List].self) { content in XCTAssertEqual(content.count, 9) } .test() } func testCreate() async throws { let app = try createTestApp() let token = try authenticateRoot(app) defer { app.shutdown() } let category = try await BlogCategoryModel.query(on: app.db).first() guard let category = category else { XCTFail("Missing default category") throw Abort(.notFound) } let newPost = Blog.Post.Create( title: "Dummy post", slug: "dummy-slug", image: "/dummy/image.jpg", excerpt: "Lorem ipsum", date: Date(), content: "Lorem ipsum", categoryId: category.id! ) try app .describe("Create post should be fine") .post("/api/blog/posts/") .body(newPost) .bearerToken(token.value) .expect(.created) .expect(.json) .expect(Blog.Post.Detail.self) { content in XCTAssertEqual(content.title, newPost.title) } .test() } func testUpdate() async throws { let app = try self.createTestApp() let token = try authenticateRoot(app) defer { app.shutdown() } guard let post = try await BlogPostModel .query(on: app.db) .with(\.$category) .first() else { XCTFail("Missing blog post") throw Abort(.notFound) } let suffix = " updated" let newPost = Blog.Post.Update( title: post.title + suffix, slug: post.slug + suffix, image: post.imageKey + suffix, excerpt: post.excerpt + suffix, date: post.date, content: post.content + suffix, categoryId: post.category.id! ) try app .describe("Update post should be fine") .put("/api/blog/posts/\(post.id!.uuidString)/") .body(newPost) .bearerToken(token.value) .expect(.ok) .expect(.json) .expect(Blog.Post.Detail.self) { content in XCTAssertEqual(content.id, post.id) XCTAssertEqual(content.title, newPost.title) XCTAssertEqual(content.slug, newPost.slug) XCTAssertEqual(content.image, newPost.image) XCTAssertEqual(content.excerpt, newPost.excerpt) XCTAssertEqual(content.content, newPost.content) } .test() } } ================================================ FILE: Chapter 15/myProject/Tests/AppTests/Framework/AppTestCase.swift ================================================ @testable import App import XCTVapor class AppTestCase: XCTestCase { struct UserLogin: Content { let email: String let password: String } func createTestApp() throws -> Application { let app = Application(.testing) try configure(app) app.databases.reinitialize() app.databases.use(.sqlite(.memory), as: .sqlite) app.databases.default(to: .sqlite) try app.autoMigrate().wait() return app } func authenticate( _ user: UserLogin, _ app: Application ) throws -> User.Token.Detail { var token: User.Token.Detail? try app.test(.POST, "/api/sign-in/", beforeRequest: { req in try req.content.encode(user) }, afterResponse: { res in XCTAssertContent(User.Token.Detail.self, res) { content in token = content } }) guard let result = token else { XCTFail("Login failed") throw Abort(.unauthorized) } return result } func authenticateRoot( _ app: Application ) throws -> User.Token.Detail { try authenticate( .init( email: "root@localhost.com", password: "ChangeMe1" ), app ) } } ================================================ FILE: Chapter 15/myProject/Tests/AppTests/Framework/XCTApplicationTester.swift ================================================ import XCTVapor extension XCTApplicationTester { @discardableResult public func test( _ method: HTTPMethod, _ path: String, headers: HTTPHeaders = [:], content: T, afterResponse: (XCTHTTPResponse) throws -> () = { _ in } ) throws -> XCTApplicationTester where T: Content { try test(method, path, headers: headers, beforeRequest: { req in try req.content.encode(content) }, afterResponse: afterResponse) } } ================================================ FILE: Chapter 15/myProject/docker-compose.yml ================================================ # 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 # Stop all: docker compose down # x-shared_environment: &shared_environment LOG_LEVEL: ${LOG_LEVEL:-debug} services: app: image: myproject:latest build: context: . environment: <<: *shared_environment 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"] ================================================ FILE: Chapter 16/myProject/Dockerfile ================================================ # ================================ # Build image # ================================ FROM swift:6.0-noble 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 \ $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true) # Copy entire repo into container COPY . . # Build the application, 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 \ --product myProject \ --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)/myProject" ./ # 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:noble # 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 ["./myProject"] CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] ================================================ FILE: Chapter 16/myProject/Package.swift ================================================ // swift-tools-version:5.7 import PackageDescription let package = Package( name: "myProject", platforms: [ .macOS(.v12) ], products: [ .library(name: "AppApi", targets: ["AppApi"]), ], dependencies: [ .package( url: "https://github.com/vapor/vapor", from: "4.70.0" ), .package( url: "https://github.com/vapor/fluent", from: "4.4.0" ), .package( url: "https://github.com/vapor/fluent-sqlite-driver", from: "4.1.0" ), .package( url: "https://github.com/binarybirds/liquid", from: "1.3.0" ), .package( url: "https://github.com/binarybirds/liquid-local-driver", from: "1.3.0" ), .package( url: "https://github.com/binarybirds/swift-html", from: "1.7.0" ), .package( url: "https://github.com/binarybirds/spec", from: "1.2.0" ), ], targets: [ .target(name: "AppApi", dependencies: []), .target(name: "App", dependencies: [ .product(name: "Vapor", package: "vapor"), .product(name: "Fluent", package: "fluent"), .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), .product(name: "Liquid", package: "liquid"), .product(name: "LiquidLocalDriver", package: "liquid-local-driver"), .product(name: "SwiftHtml", package: "swift-html"), .product(name: "SwiftSvg", package: "swift-html"), .target(name: "AppApi") ]), .executableTarget(name: "Run", dependencies: ["App"]), .testTarget(name: "AppTests", dependencies: [ .target(name: "App"), .product(name: "XCTVapor", package: "vapor"), .product(name: "Spec", package: "spec"), ]), .testTarget(name: "AppApiTests", dependencies: [ .target(name: "AppApi"), ]), ] ) ================================================ FILE: Chapter 16/myProject/Public/css/admin.css ================================================ tr { column-gap: 1rem; } ================================================ FILE: Chapter 16/myProject/Public/css/web.css ================================================ #blog h2 { margin: 0.5rem 0; } ================================================ FILE: Chapter 16/myProject/Public/js/admin.js ================================================ document.addEventListener("keydown", function(e) { if ( (window.navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey) && e.keyCode == 83 ) { e.preventDefault(); document.forms[0].submit(); } }, false); ================================================ FILE: Chapter 16/myProject/Public/js/web.js ================================================ function about() { alert("myPage\n\nversion 1.0.0"); } ================================================ FILE: Chapter 16/myProject/Sources/App/Extensions/Svg+MenuIcon.swift ================================================ import SwiftSvg extension Svg { static func menuIcon() -> Svg { Svg { Line(x1: 3, y1: 12, x2: 21, y2: 12) Line(x1: 3, y1: 6, x2: 21, y2: 6) Line(x1: 3, y1: 18, x2: 21, y2: 18) } .width(24) .height(24) .viewBox(minX: 0, minY: 0, width: 24, height: 24) .fill("none") .stroke("currentColor") .strokeWidth(2) .strokeLinecap("round") .strokeLinejoin("round") } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/ApiModelInterface+PathComponent.swift ================================================ import Vapor import AppApi extension ApiModelInterface { static var pathIdComponent: PathComponent { .init(stringLiteral: ":" + pathIdKey) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/AuthenticatedUser.swift ================================================ import Vapor public struct AuthenticatedUser { public let id: UUID public let email: String public init( id: UUID, email: String ) { self.id = id self.email = email } } extension AuthenticatedUser: SessionAuthenticatable { public var sessionID: UUID { id } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Controllers/CreateController.swift ================================================ import Vapor protocol CreateController: ModelController { func create( _ req: Request, _ model: DatabaseModel ) async throws func beforeCreate( _ req: Request, _ model: DatabaseModel ) async throws func afterCreate( _ req: Request, _ model: DatabaseModel ) async throws } extension CreateController { func create( _ req: Request, _ model: DatabaseModel ) async throws { try await beforeCreate(req, model) try await model.create(on: req.db) try await afterCreate(req, model) } func beforeCreate( _ req: Request, _ model: DatabaseModel ) async throws {} func afterCreate( _ req: Request, _ model: DatabaseModel ) async throws {} } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Controllers/DeleteController.swift ================================================ import Vapor public protocol DeleteController: ModelController { func delete( _ req: Request, _ model: DatabaseModel ) async throws func beforeDelete( _ req: Request, _ model: DatabaseModel ) async throws func afterDelete( _ req: Request, _ model: DatabaseModel ) async throws } public extension DeleteController { func delete( _ req: Request, _ model: DatabaseModel ) async throws { try await beforeDelete(req, model) try await model.delete(on: req.db) try await afterDelete(req, model) } func beforeDelete( _ req: Request, _ model: DatabaseModel ) async throws {} func afterDelete( _ req: Request, _ model: DatabaseModel ) async throws {} } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Controllers/DetailController.swift ================================================ import Vapor import Fluent protocol DetailController: ModelController { func detail( _ req: Request ) async throws -> DatabaseModel func beforeDetail( _ req: Request, _ queryBuilder: QueryBuilder ) async throws -> QueryBuilder func afterDetail( _ req: Request, _ model: DatabaseModel ) async throws -> DatabaseModel } extension DetailController { func detail( _ req: Request ) async throws -> DatabaseModel { let queryBuilder = DatabaseModel.query(on: req.db) let model = try await beforeDetail(req, queryBuilder) .filter(\._$id == identifier(req)) .first() guard let model = model else { throw Abort(.notFound) } return try await afterDetail(req, model) } func beforeDetail( _ req: Request, _ queryBuilder: QueryBuilder ) async throws -> QueryBuilder { queryBuilder } func afterDetail( _ req: Request, _ model: DatabaseModel ) async throws -> DatabaseModel { model } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Controllers/ListController.swift ================================================ import Vapor import Fluent protocol ListController: ModelController { func list( _ req: Request ) async throws -> [DatabaseModel] func beforeList( _ req: Request, _ queryBuilder: QueryBuilder ) async throws -> QueryBuilder func afterList( _ req: Request, _ models: [DatabaseModel] ) async throws -> [DatabaseModel] } extension ListController { func list( _ req: Request ) async throws -> [DatabaseModel] { try await DatabaseModel .query(on: req.db) .all() } func beforeList( _ req: Request, _ queryBuilder: QueryBuilder ) async throws -> QueryBuilder { queryBuilder } func afterList( _ req: Request, _ models: [DatabaseModel] ) async throws -> [DatabaseModel] { models } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Controllers/ModelController.swift ================================================ import Vapor import Fluent public struct Name { let singular: String let plural: String init( singular: String, plural: String? = nil ) { self.singular = singular self.plural = plural ?? singular + "s" } } public protocol ModelController { associatedtype ApiModel: ApiModelInterface associatedtype DatabaseModel: DatabaseModelInterface static var moduleName: String { get } static var modelName: Name { get } func identifier( _ req: Request ) throws -> UUID func findBy( _ id: UUID, on: Database ) async throws -> DatabaseModel func getBaseRoutes( _ routes: RoutesBuilder ) -> RoutesBuilder } extension ModelController { static var moduleName: String { DatabaseModel.Module.identifier.capitalized } static var modelName: Name { .init(singular: String(DatabaseModel.identifier.dropLast(1))) } func identifier( _ req: Request ) throws -> UUID { guard let id = req.parameters.get(ApiModel.pathIdKey), let uuid = UUID(uuidString: id) else { throw Abort(.badRequest) } return uuid } func findBy( _ id: UUID, on db: Database ) async throws -> DatabaseModel { guard let model = try await DatabaseModel.find(id, on: db) else { throw Abort(.notFound) } return model } func getBaseRoutes( _ routes: RoutesBuilder ) -> RoutesBuilder { routes .grouped(ApiModel.Module.pathKey.pathComponents) .grouped(ApiModel.pathKey.pathComponents) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Controllers/PatchController.swift ================================================ import Vapor public protocol PatchController: ModelController { func patch( _ req: Request, _ model: DatabaseModel ) async throws func beforePatch( _ req: Request, _ model: DatabaseModel ) async throws func afterPatch( _ req: Request, _ model: DatabaseModel ) async throws } public extension PatchController { func patch( _ req: Request, _ model: DatabaseModel ) async throws { try await beforePatch(req, model) try await model.update(on: req.db) try await afterPatch(req, model) } func beforePatch( _ req: Request, _ model: DatabaseModel ) async throws {} func afterPatch( _ req: Request, _ model: DatabaseModel ) async throws {} } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Controllers/UpdateController.swift ================================================ import Vapor public protocol UpdateController: ModelController { func update( _ req: Request, _ model: DatabaseModel ) async throws func beforeUpdate( _ req: Request, _ model: DatabaseModel ) async throws func afterUpdate( _ req: Request, _ model: DatabaseModel ) async throws } public extension UpdateController { func update( _ req: Request, _ model: DatabaseModel ) async throws { try await beforeUpdate(req, model) try await model.update(on: req.db) try await afterUpdate(req, model) } func beforeUpdate( _ req: Request, _ model: DatabaseModel ) async throws {} func afterUpdate( _ req: Request, _ model: DatabaseModel ) async throws {} } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/DatabaseModelInterface.swift ================================================ import Vapor import Fluent public protocol DatabaseModelInterface: Fluent.Model where Self.IDValue == UUID { associatedtype Module: ModuleInterface static var identifier: String { get } } public extension DatabaseModelInterface { static var schema: String { Module.identifier + "_" + identifier } static var identifier: String { String(describing: self) .dropFirst(Module.identifier.count) .dropLast(5) .lowercased() + "s" } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Extensions/ByteBuffer+Data.swift ================================================ import Vapor public extension ByteBuffer { var data: Data? { getData(at: 0, length: readableBytes) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Extensions/File+ByteBuffer.swift ================================================ import Vapor public extension File { var byteBuffer: ByteBuffer { data } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Form/AbstractForm.swift ================================================ import Vapor open class AbstractForm: FormComponent { open var action: FormAction open var fields: [FormComponent] open var error: String? open var submit: String? public init( action: FormAction = .init(), fields: [FormComponent] = [], error: String? = nil, submit: String? = nil ) { self.action = action self.fields = fields self.error = error self.submit = submit self.action.enctype = .multipart } open func load(req: Request) async throws { for field in fields { try await field.load(req: req) } } open func process(req: Request) async throws { for field in fields { try await field.process(req: req) } } open func validate(req: Request) async throws -> Bool { var result: [Bool] = [] for field in fields { result.append(try await field.validate(req: req)) } return result.filter { $0 == false }.isEmpty } open func write(req: Request) async throws { for field in fields { try await field.write(req: req) } } open func save(req: Request) async throws { for field in fields { try await field.save(req: req) } } open func read(req: Request) async throws { for field in fields { try await field.read(req: req) } } open func render(req: Request) -> TemplateRepresentable { FormTemplate(getContext(req)) } func getContext(_ req: Request) -> FormContext { .init( action: action, fields: fields.map { $0.render(req: req)}, error: error, submit: submit ) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Form/AbstractFormField.swift ================================================ import Vapor open class AbstractFormField< Input: Decodable, Output: TemplateRepresentable >: FormComponent { public var key: String public var input: Input public var output: Output public var error: String? // MARK: - event blocks public typealias FormFieldBlock = (Request, AbstractFormField) async throws -> Void public typealias FormFieldValidatorsBlock = ((Request, AbstractFormField) -> [AsyncValidator]) private var readBlock: FormFieldBlock? private var writeBlock: FormFieldBlock? private var loadBlock: FormFieldBlock? private var saveBlock: FormFieldBlock? private var validatorsBlock: FormFieldValidatorsBlock? // MARK: - init & config public init( key: String, input: Input, output: Output, error: String? = nil ) { self.key = key self.input = input self.output = output self.error = error } open func config( _ block: (AbstractFormField) -> Void ) -> Self { block(self) return self } // MARK: - Block setters open func read(_ block: @escaping FormFieldBlock) -> Self { readBlock = block return self } open func write(_ block: @escaping FormFieldBlock) -> Self { writeBlock = block return self } open func load(_ block: @escaping FormFieldBlock) -> Self { loadBlock = block return self } open func save(_ block: @escaping FormFieldBlock) -> Self { saveBlock = block return self } open func validators( @AsyncValidatorBuilder _ block: @escaping FormFieldValidatorsBlock ) -> Self { validatorsBlock = block return self } // MARK: - FormComponent open func process(req: Request) async throws { if let value = try? req.content.get(Input.self, at: key) { input = value } } open func validate(req: Request) async throws -> Bool { guard let validators = validatorsBlock else { return true } return await RequestValidator(validators(req, self)).isValid(req) } open func read(req: Request) async throws { try await readBlock?(req, self) } open func write(req: Request) async throws { try await writeBlock?(req, self) } open func load(req: Request) async throws { try await loadBlock?(req, self) } open func save(req: Request) async throws { try await saveBlock?(req, self) } open func render(req: Request) -> TemplateRepresentable { output } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Form/Fields/HiddenField.swift ================================================ import Vapor public final class HiddenField: AbstractFormField< String, HiddenFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Form/Fields/ImageField.swift ================================================ import Vapor public final class ImageField: AbstractFormField< FormImageInput, ImageFieldTemplate > { public var imageKey: String? { didSet { output.context.data.originalKey = imageKey } } public var path: String public init( _ key: String, path: String ) { self.path = path super.init( key: key, input: .init(key: key), output: .init(.init(key: key)) ) } public override func process( req: Request ) async throws { /// process input input.file = try? req.content.get( File.self, at: key ) input.data.originalKey = try? req.content.get( String.self, at: key + "OriginalKey" ) if let temporaryFileKey = try? req.content.get( String.self, at: key + "TemporaryFileKey" ), let temporaryFileName = try? req.content.get( String.self, at: key + "TemporaryFileName" ) { input.data.temporaryFile = .init( key: temporaryFileKey, name: temporaryFileName ) } input.data.shouldRemove = (try? req.content.get( Bool.self, at: key + "ShouldRemove" )) ?? false /// remove & upload file if input.data.shouldRemove { if let originalKey = input.data.originalKey { try? await req.fs.delete(key: originalKey) } } else if let file = input.file, let data = file.byteBuffer.data, !data.isEmpty { if let tmpKey = input.data.temporaryFile?.key { try? await req.fs.delete(key: tmpKey) } let key = "tmp/\(UUID().uuidString).tmp" _ = try await req.fs.upload(key: key, data: data) /// update the temporary image input.data.temporaryFile = .init( key: key, name: file.filename ) } /// update output values output.context.data = input.data } public override func write( req: Request ) async throws { imageKey = input.data.originalKey if input.data.shouldRemove { if let key = input.data.originalKey { try? await req.fs.delete(key: key) } imageKey = nil } else if let file = input.data.temporaryFile { var newKey = path + "/" + file.name if await req.fs.exists(key: newKey) { let formatter = DateFormatter() formatter.dateFormat="y-MM-dd-HH-mm-ss-" let prefix = formatter.string(from: .init()) newKey = path + "/" + prefix + file.name } _ = try await req.fs.move(key: file.key, to: newKey) input.data.temporaryFile = nil if let key = input.data.originalKey { try? await req.fs.delete(key: key) } imageKey = newKey } try await super.write(req: req) } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Form/Fields/InputField.swift ================================================ public final class InputField: AbstractFormField< String, InputFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init( key: key ) ) ) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Form/Fields/SelectField.swift ================================================ import Vapor public final class SelectField: AbstractFormField< String, SelectFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Form/Fields/TextareaField.swift ================================================ import Vapor public final class TextareaField: AbstractFormField< String, TextareaFieldTemplate > { public convenience init(_ key: String) { self.init( key: key, input: "", output: .init( .init(key: key) ) ) } public override func process( req: Request ) async throws { try await super.process(req: req) output.context.value = input } public override func render( req: Request ) -> TemplateRepresentable { output.context.error = error return super.render(req: req) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Form/FormAction.swift ================================================ import SwiftHtml public struct FormAction { public var method: SwiftHtml.Method public var url: String? public var enctype: SwiftHtml.Enctype? public init( method: SwiftHtml.Method = .post, url: String? = nil, enctype: SwiftHtml.Enctype? = nil ) { self.method = method self.url = url self.enctype = enctype } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Form/FormComponent.swift ================================================ import Vapor public protocol FormComponent { func load(req: Request) async throws func process(req: Request) async throws func validate(req: Request) async throws -> Bool func write(req: Request) async throws func save(req: Request) async throws func read(req: Request) async throws func render(req: Request) -> TemplateRepresentable } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Form/FormComponentBuilder.swift ================================================ @resultBuilder public enum FormComponentBuilder { public static func buildBlock( _ components: FormComponent... ) -> [FormComponent] { components } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Form/FormImageData.swift ================================================ public struct FormImageData: Codable { public struct TemporaryFile: Codable { public let key: String public let name: String public init( key: String, name: String ) { self.key = key self.name = name } } public var originalKey: String? public var temporaryFile: TemporaryFile? public var shouldRemove: Bool public init( originalKey: String? = nil, temporaryFile: TemporaryFile? = nil, shouldRemove: Bool = false ) { self.originalKey = originalKey self.temporaryFile = temporaryFile self.shouldRemove = shouldRemove } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Form/FormImageInput.swift ================================================ import Vapor public struct FormImageInput: Codable { public var key: String public var file: File? public var data: FormImageData public init( key: String, file: File? = nil, data: FormImageData? = nil ) { self.key = key self.file = file self.data = data ?? .init() } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Hooks/Application+HookStorage.swift ================================================ import Vapor extension Application { private struct HookStorageKey: StorageKey { typealias Value = HookStorage } var hooks: HookStorage { get { if let existing = storage[HookStorageKey.self] { return existing } let new = HookStorage() storage[HookStorageKey.self] = new return new } set { storage[HookStorageKey.self] = newValue } } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Hooks/Async/Application+AsyncHooks.swift ================================================ import Vapor extension Application { func invokeAsync( _ name: String, args: HookArguments = [:] ) async throws -> ReturnType? { let ctxArgs = args.merging(["app": self]) { (_, new) in new } return try await hooks.invokeAsync(name, args: ctxArgs) } func invokeAllAsync( _ name: String, args: HookArguments = [:] ) async throws -> [ReturnType] { let ctxArgs = args.merging(["app": self]) { (_, new) in new } return try await hooks.invokeAllAsync(name, args: ctxArgs) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Hooks/Async/AsyncAnyHookFunction.swift ================================================ struct AsyncAnyHookFunction: AsyncHookFunction { private let functionBlock: AsyncHookFunctionSignature init(_ functionBlock: @escaping AsyncHookFunctionSignature) { self.functionBlock = functionBlock } func invokeAsync(_ args: HookArguments) async throws -> Any { try await functionBlock(args) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Hooks/Async/AsyncHookFunction.swift ================================================ protocol AsyncHookFunction { func invokeAsync(_: HookArguments) async throws -> Any } typealias AsyncHookFunctionSignature = (HookArguments) async throws -> T ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Hooks/Async/HookStorage+AsyncHooks.swift ================================================ extension HookStorage { func registerAsync( _ name: String, use block: @escaping AsyncHookFunctionSignature ) { let function = AsyncAnyHookFunction { args -> Any in try await block(args) } let pointer = HookFunctionPointer( name: name, function: function, returnType: ReturnType.self ) asyncPointers.append(pointer) } func invokeAsync( _ name: String, args: HookArguments = [:] ) async throws -> ReturnType? { try await asyncPointers.first { $0.name == name && $0.returnType == ReturnType.self }?.pointer.invokeAsync(args) as? ReturnType } func invokeAllAsync( _ name: String, args: HookArguments = [:] ) async throws -> [ReturnType] { let fns = asyncPointers.filter { $0.name == name && $0.returnType == ReturnType.self } var result: [ReturnType] = [] for fn in fns { if let res = try await fn.pointer.invokeAsync(args) as? ReturnType { result.append(res) } } return result } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Hooks/Async/Request+AsyncHooks.swift ================================================ import Vapor extension Request { func invokeAsync( _ name: String, args: HookArguments = [:] ) async throws -> ReturnType? { let ctxArgs = args.merging(["req": self]) { (_, new) in new } return try await application.invokeAsync(name, args: ctxArgs) } func invokeAllAsync( _ name: String, args: HookArguments = [:] ) async throws -> [ReturnType] { let ctxArgs = args.merging(["req": self]) { (_, new) in new } return try await application.invokeAllAsync(name, args: ctxArgs) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Hooks/HookArguments.swift ================================================ typealias HookArguments = [String: Any] ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Hooks/HookFunctionPointer.swift ================================================ final class HookFunctionPointer { var name: String var pointer: Pointer var returnType: Any.Type init( name: String, function: Pointer, returnType: Any.Type ) { self.name = name self.pointer = function self.returnType = returnType } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Hooks/HookStorage.swift ================================================ final class HookStorage { var pointers: [HookFunctionPointer] var asyncPointers: [HookFunctionPointer] init() { self.pointers = [] self.asyncPointers = [] } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Hooks/Sync/AnyHookFunction.swift ================================================ struct AnyHookFunction: HookFunction { private let functionBlock: HookFunctionSignature init(_ functionBlock: @escaping HookFunctionSignature) { self.functionBlock = functionBlock } func invoke(_ args: HookArguments) -> Any { functionBlock(args) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Hooks/Sync/Application+Hooks.swift ================================================ import Vapor extension Application { func invoke( _ name: String, args: HookArguments = [:] ) -> ReturnType? { let ctxArgs = args.merging(["app": self]) { (_, new) in new } return hooks.invoke(name, args: ctxArgs) } func invokeAll( _ name: String, args: HookArguments = [:] ) -> [ReturnType] { let ctxArgs = args.merging(["app": self]) { (_, new) in new } return hooks.invokeAll(name, args: ctxArgs) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Hooks/Sync/HookFunction.swift ================================================ protocol HookFunction { func invoke(_: HookArguments) -> Any } typealias HookFunctionSignature = (HookArguments) -> T ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Hooks/Sync/HookStorage+Hooks.swift ================================================ extension HookStorage { func register( _ name: String, use block: @escaping HookFunctionSignature ) { let function = AnyHookFunction { args -> Any in block(args) } let pointer = HookFunctionPointer( name: name, function: function, returnType: ReturnType.self ) pointers.append(pointer) } func invoke( _ name: String, args: HookArguments = [:] ) -> ReturnType? { pointers.first { $0.name == name && $0.returnType == ReturnType.self }?.pointer.invoke(args) as? ReturnType } func invokeAll( _ name: String, args: HookArguments = [:] ) -> [ReturnType] { pointers.filter { $0.name == name && $0.returnType == ReturnType.self } .compactMap { $0.pointer.invoke(args) as? ReturnType } } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Hooks/Sync/Request+Hooks.swift ================================================ import Vapor extension Request { func invoke( _ name: String, args: HookArguments = [:] ) -> ReturnType? { let ctxArgs = args.merging(["req": self]) { (_, new) in new } return application.invoke(name, args: ctxArgs) } func invokeAll( _ name: String, args: HookArguments = [:] ) -> [ReturnType] { let ctxArgs = args.merging(["req": self]) { (_, new) in new } return application.invokeAll(name, args: ctxArgs) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/ModelEditorInterface.swift ================================================ import Vapor public protocol ModelEditorInterface: FormComponent { associatedtype Model: DatabaseModelInterface var model: Model { get } var form: AbstractForm { get } init(model: Model, form: AbstractForm) @FormComponentBuilder var formFields: [FormComponent] { get } } public extension ModelEditorInterface { func load(req: Request) async throws { try await form.load(req: req) } func process(req: Request) async throws { try await form.process(req: req) } func validate(req: Request) async throws -> Bool { try await form.validate(req: req) } func write(req: Request) async throws { try await form.write(req: req) } func save(req: Request) async throws { try await form.save(req: req) } func read(req: Request) async throws { try await form.read(req: req) } func render(req: Request) -> TemplateRepresentable { form.render(req: req) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/ModuleInterface.swift ================================================ import Vapor public protocol ModuleInterface { static var identifier: String { get } func boot(_ app: Application) throws func setUp(_ app: Application) throws } public extension ModuleInterface { func boot(_ app: Application) throws {} func setUp(_ app: Application) throws {} static var identifier: String { String(describing: self).dropLast(6).lowercased() } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Contexts/CellContext.swift ================================================ public struct CellContext { public enum `Type`: String { case text case image } public let value: String public let link: LinkContext? public let type: `Type` public init( _ value: String, link: LinkContext? = nil, type: `Type` = .text ) { self.type = type self.value = value self.link = link } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Contexts/ColumnContext.swift ================================================ public struct ColumnContext { public let key: String public let label: String public init( _ key: String, label: String? = nil ) { self.key = key self.label = label ?? key.capitalized } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Contexts/DetailContext.swift ================================================ public struct DetailContext { public enum `Type`: String { case text case image } public let key: String public let label: String public let value: String public let type: `Type` public init( _ key: String, _ value: String, label: String? = nil, type: `Type` = .text ) { self.key = key self.label = label ?? key.capitalized self.value = value self.type = type } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Contexts/FormContext.swift ================================================ public struct FormContext { public var action: FormAction public var fields: [TemplateRepresentable] public var error: String? public var submit: String? } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Contexts/HiddenFieldContext.swift ================================================ public struct HiddenFieldContext { public let key: String public var value: String? public init( key: String, value: String? = nil ) { self.key = key self.value = value } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Contexts/ImageFieldContext.swift ================================================ public struct ImageFieldContext { public let key: String public var label: LabelContext public var data: FormImageData public var previewUrl: String? public var accept: String? public var error: String? public init( key: String, label: LabelContext? = nil, data: FormImageData = .init(), previewUrl: String? = nil, accept: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.data = data self.previewUrl = previewUrl self.accept = accept self.error = error } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Contexts/InputFieldContext.swift ================================================ import SwiftHtml public struct InputFieldContext { public let key: String public var label: LabelContext public var type: SwiftHtml.Input.`Type` public var placeholder: String? public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, type: Input.`Type` = .text, placeholder: String? = nil, value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.type = type self.placeholder = placeholder self.value = value self.error = error } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Contexts/LabelContext.swift ================================================ public struct LabelContext { public var key: String public var title: String? public var required: Bool public var more: String? public init( key: String, title: String? = nil, required: Bool = false, more: String? = nil ) { self.key = key self.title = title self.required = required self.more = more } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Contexts/LinkContext.swift ================================================ import Vapor public struct LinkContext { public let label: String public let path: String public let absolute: Bool public let isBlank: Bool public let dropLast: Int public init( label: String, path: String = "", absolute: Bool = false, isBlank: Bool = false, dropLast: Int = 0 ) { self.label = label self.path = path self.absolute = absolute self.isBlank = isBlank self.dropLast = dropLast } public func url( _ req: Request, _ infix: [PathComponent] = [] ) -> String { if absolute { return path } return "/" + (req.url.path.pathComponents.dropLast(dropLast) + (infix + path.pathComponents)).string } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Contexts/OptionContext.swift ================================================ public struct OptionContext { public var key: String public var label: String public init( key: String, label: String ) { self.key = key self.label = label } } public extension OptionContext { static func yesNo() -> [OptionContext] { ["yes", "no"].map { .init(key: $0, label: $0.capitalized) } } static func trueFalse() -> [OptionContext] { [true, false].map { .init(key: String($0), label: String($0).capitalized) } } static func numbers( _ numbers: [Int] ) -> [OptionContext] { numbers.map { .init(key: String($0), label: String($0)) } } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Contexts/RowContext.swift ================================================ public struct RowContext { public let id: String public let cells: [CellContext] public init( id: String, cells: [CellContext] ) { self.id = id self.cells = cells } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Contexts/SelectFieldContext.swift ================================================ public struct SelectFieldContext { public let key: String public var label: LabelContext public var options: [OptionContext] public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, options: [OptionContext] = [], value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.options = options self.value = value self.error = error } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Contexts/TableContext.swift ================================================ public struct TableContext { public let columns: [ColumnContext] public let rows: [RowContext] public let actions: [LinkContext] public init( columns: [ColumnContext], rows: [RowContext], actions: [LinkContext] = [] ) { self.columns = columns self.rows = rows self.actions = actions } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Contexts/TextareaFieldContext.swift ================================================ public struct TextareaFieldContext { public let key: String public var label: LabelContext public var placeholder: String? public var value: String? public var error: String? public init( key: String, label: LabelContext? = nil, placeholder: String? = nil, value: String? = nil, error: String? = nil ) { self.key = key self.label = label ?? .init(key: key) self.placeholder = placeholder self.value = value self.error = error } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Html/CellTemplate.swift ================================================ import Vapor import SwiftHtml public struct CellTemplate: TemplateRepresentable { var context: CellContext var rowId: String public init( _ context: CellContext, rowId: String ) { self.context = context self.rowId = rowId } @TagBuilder public func render( _ req: Request ) -> Tag { Td { switch context.type { case .text: if let link = context.link { LinkTemplate(link, pathInfix: rowId).render(req) } else { Text(context.value) } case .image: if let link = context.link { LinkTemplate(link, pathInfix: rowId) { label in Img(src: context.value, alt: label) } .render(req) } else { Img(src: context.value, alt: context.value) } } } .class("field") } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Html/DetailTemplate.swift ================================================ import Vapor import SwiftHtml public struct DetailTemplate: TemplateRepresentable { var context: DetailContext public init( _ context: DetailContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Dt(context.label) switch context.type { case .text: if context.value.isEmpty { Dd(" ") } else { Dd( context.value.replacingOccurrences( of: "\n", with: "
" ) ) } case .image: Dd { Img( src: context.value, alt: context.label ) } } } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Html/FormTemplate.swift ================================================ import Vapor import SwiftHtml public struct FormTemplate: TemplateRepresentable { var context: FormContext public init( _ context: FormContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Form { if let error = context.error { Section { P(error) .class("error") } } for field in context.fields { Section { field.render(req) } } Section { Input() .type(.submit) .value(context.submit ?? "Save") } } .method(context.action.method) .action(context.action.url) .enctype(context.action.enctype) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Html/HiddenFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct HiddenFieldTemplate: TemplateRepresentable { var context: HiddenFieldContext public init( _ context: HiddenFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Input() .type(.hidden) .name(context.key) .value(context.value) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Html/ImageFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct ImageFieldTemplate: TemplateRepresentable { public var context: ImageFieldContext public init( _ context: ImageFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { if let url = context.previewUrl { Img(src: req.fs.resolve(key: url), alt: context.key) } else { Img(src: "/img/logo.png", alt: "default post image") } LabelTemplate(context.label).render(req) Input() .type(.file) .key(context.key) .class("field") .accept(context.accept) if let temporaryFile = context.data.temporaryFile { Input() .key(context.key + "TemporaryFileKey") .value(temporaryFile.key) .type(.hidden) Input() .key(context.key + "TemporaryFileName") .value(temporaryFile.name) .type(.hidden) } if let key = context.data.originalKey { Input() .key(context.key + "OriginalKey") .value(key) .type(.hidden) } if !context.label.required { Input() .key(context.key + "ShouldRemove") .value(String(true)) .type(.checkbox) .checked(context.data.shouldRemove) Label("Remove") .for(context.key + "Remove") } if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Html/InputFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct InputFieldTemplate: TemplateRepresentable { public var context: InputFieldContext public init( _ context: InputFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Input() .type(context.type) .key(context.key) .placeholder(context.placeholder) .value(context.value) .class("field") if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Html/LabelTemplate.swift ================================================ import Vapor import SwiftHtml public struct LabelTemplate: TemplateRepresentable { var context: LabelContext public init( _ context: LabelContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Label { Text(context.title ?? context.key.capitalized) if let more = context.more { Span(more) .class("more") } if context.required { Span("*") .class("required") } }.for(context.key) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Html/LinkTemplate.swift ================================================ import Vapor import SwiftHtml public struct LinkTemplate: TemplateRepresentable { var context: LinkContext var body: Tag var pathInfix: String? public init( _ context: LinkContext, pathInfix: String? = nil, _ builder: ((String) -> Tag)? = nil ) { self.context = context self.pathInfix = pathInfix self.body = builder?(context.label) ?? Text(context.label) } @TagBuilder public func render( _ req: Request ) -> Tag { A { body } .href(context.url(req, pathInfix?.pathComponents ?? [])) .target(.blank, context.isBlank) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Html/SelectFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct SelectFieldTemplate: TemplateRepresentable { public var context: SelectFieldContext public init( _ context: SelectFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Select { for item in context.options { Option(item.label) .value(item.key) .selected(context.value == item.key) } } .name(context.key) if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Html/TableTemplate.swift ================================================ import Vapor import SwiftHtml public struct TableTemplate: TemplateRepresentable { var context: TableContext public init( _ context: TableContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { Table { Thead { Tr { context.columns.map { column in Th(column.label) .id(column.key) .class("field") } context.actions.map { action in Th(action.label) .class("action") } } } Tbody { for row in context.rows { Tr { row.cells.map { CellTemplate($0, rowId: row.id).render(req) } context.actions.map { action in Td { LinkTemplate(action, pathInfix: row.id).render(req) } .class("action") } } .id(row.id) } } } } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Templates/Html/TextareaFieldTemplate.swift ================================================ import Vapor import SwiftHtml public struct TextareaFieldTemplate: TemplateRepresentable { public var context: TextareaFieldContext public init( _ context: TextareaFieldContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { LabelTemplate(context.label).render(req) Textarea(context.value) .placeholder(context.placeholder) .name(context.key) if let error = context.error { Span(error) .class("error") } } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Validation/AsyncValidator.swift ================================================ import Vapor public protocol AsyncValidator { var key: String { get } var message: String { get } func validate( _ req: Request ) async throws -> ValidationErrorDetail? } public extension AsyncValidator { var error: ValidationErrorDetail { .init(key: key, message: message) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Validation/AsyncValidatorBuilder.swift ================================================ @resultBuilder public enum AsyncValidatorBuilder { public static func buildBlock( _ components: AsyncValidator... ) -> [AsyncValidator] { components } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Validation/FormFieldValidator+Validations.swift ================================================ import Vapor public extension FormFieldValidator where Input == String { static func required( _ field: AbstractFormField, _ message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is required" return .init(field, msg) { _, field in !field.input.isEmpty } } static func min( _ field: AbstractFormField, length: Int, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)" return .init(field, msg) { _, field in field.input.count >= length } } static func max( _ field: AbstractFormField, length: Int, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) is too short (min: \(length) characters)" return .init(field, msg) { _, field in field.input.count <= length } } static func alphanumeric( _ field: AbstractFormField, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) should be only alphanumeric characters" return .init(field, msg) { _, field in !Validator .characterSet(.alphanumerics) .validate(field.input) .isFailure } } static func email( _ field: AbstractFormField, message: String? = nil ) -> FormFieldValidator { let msg = message ?? "\(field.key.capitalized) should be a valid email address" return .init(field, msg) { _, field in !Validator .email .validate(field.input) .isFailure } } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Validation/FormFieldValidator.swift ================================================ import Vapor public struct FormFieldValidator< Input: Decodable, Output: TemplateRepresentable >: AsyncValidator { public typealias AsyncValidationBlock = ((Request, AbstractFormField) async throws -> Bool) public let field: AbstractFormField public let message: String public let validation: AsyncValidationBlock public var key: String { field.key } public init( _ field: AbstractFormField, _ message: String, _ validation: @escaping AsyncValidationBlock ) { self.field = field self.message = message self.validation = validation } public func validate( _ req: Request ) async throws -> ValidationErrorDetail? { let isValid = try await validation(req, field) if isValid { return nil } field.error = message return error } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Validation/KeyedContentValidator+Validations.swift ================================================ import Vapor public extension KeyedContentValidator where T == String { static func required( _ key: String, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is required" return .init(key, msg, optional: optional) { value, _ in !value.isEmpty } } static func min( _ key: String, _ length: Int, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is too short (min: \(length) characters)" return .init(key, msg, optional: optional) { value, _ in value.count >= length } } static func max( _ key: String, _ length: Int, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is too long (max: \(length) characters)" return .init(key, msg, optional: optional) { value, _ in value.count <= length } } static func alphanumeric( _ key: String, _ message: String? = nil, _ optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) should be only alphanumeric characters" return .init(key, msg, optional: optional) { value, _ in !Validator.characterSet(.alphanumerics).validate(value).isFailure } } static func email( _ key: String, _ message: String? = nil, _ optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) should be a valid email address" return .init(key, msg, optional: optional) { value, _ in !Validator.email.validate(value).isFailure } } } public extension KeyedContentValidator where T == Int { static func min( _ key: String, _ length: Int, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is too short (min: \(length) characters)" return .init(key, msg, optional: optional) { value, _ in value >= length } } static func max( _ key: String, _ length: Int, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is too long (max: \(length) characters)" return .init(key, msg, optional: optional) { value, _ in value <= length } } static func contains( _ key: String, _ values: [Int], _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is an invalid value" return .init(key, msg, optional: optional) { value, _ in values.contains(value) } } } public extension KeyedContentValidator where T == UUID { static func required( _ key: String, _ message: String? = nil, optional: Bool = false ) -> KeyedContentValidator { let msg = message ?? "\(key.capitalized) is required" return .init(key, msg, optional: optional) { _, _ in true } } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Validation/KeyedContentValidator.swift ================================================ import Vapor public struct KeyedContentValidator: AsyncValidator { public let key: String public let message: String public let optional: Bool public let validation: (T, Request) async throws -> Bool public init( _ key: String, _ message: String, optional: Bool = false, _ validation: @escaping (T, Request) async throws -> Bool ) { self.key = key self.message = message self.optional = optional self.validation = validation } public func validate( _ req: Request ) async throws -> ValidationErrorDetail? { let optionalValue = try? req.content.get(T.self, at: key) if let value = optionalValue { return try await validation(value, req) ? nil : error } return optional ? nil : error } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Validation/RequestValidator.swift ================================================ import Vapor public struct RequestValidator { public var validators: [AsyncValidator] public init( _ validators: [AsyncValidator] ) { self.validators = validators } public func validate( _ req: Request, message: String? = nil ) async throws { var result: [ValidationErrorDetail] = [] for validator in validators { if result.contains(where: { $0.key == validator.key }) { continue } if let res = try await validator.validate(req) { result.append(res) } } if !result.isEmpty { throw ValidationAbort( abort: Abort(.badRequest, reason: message), details: result ) } } public func isValid( _ req: Request ) async -> Bool { do { try await validate(req, message: nil) return true } catch { return false } } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Validation/ValidationAbort.swift ================================================ import Vapor public struct ValidationAbort: AbortError { public var abort: Abort public var message: String? public var details: [ValidationErrorDetail] public var reason: String { abort.reason } public var status: HTTPStatus { abort.status } public init( abort: Abort, message: String? = nil, details: [ValidationErrorDetail] ) { self.abort = abort self.message = message self.details = details } } ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Validation/ValidationError.swift ================================================ import Vapor struct ValidationError: Codable { let message: String? let details: [ValidationErrorDetail] init( message: String?, details: [ValidationErrorDetail] ) { self.message = message self.details = details } } extension ValidationError: Content {} ================================================ FILE: Chapter 16/myProject/Sources/App/Framework/Validation/ValidationErrorDetail.swift ================================================ import Vapor public struct ValidationErrorDetail: Codable { public var key: String public var message: String public init( key: String, message: String ) { self.key = key self.message = message } } extension ValidationErrorDetail: Content {} ================================================ FILE: Chapter 16/myProject/Sources/App/Middlewares/ExtendPathMiddleware.swift ================================================ import Vapor struct ExtendPathMiddleware: AsyncMiddleware { func respond( to req: Request, chainingTo next: AsyncResponder ) async throws -> Response { if !req.url.path.hasSuffix("/") && !req.url.path.contains(".") { return req.redirect( to: req.url.path + "/", redirectType: .permanent ) } return try await next.respond( to: req ) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Admin/AdminModule.swift ================================================ import Vapor struct AdminModule: ModuleInterface { let router = AdminRouter() func boot(_ app: Application) throws { try router.boot(routes: app.routes) app.hooks.register("admin-routes", use: router.adminRoutesHook) } func setUp(_ app: Application) throws { try router.setUpRoutesHooks(app: app) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Admin/AdminRouter.swift ================================================ import Vapor struct AdminRouter: RouteCollection { let controller = AdminFrontendController() func boot(routes: RoutesBuilder) throws {} func setUpRoutesHooks(app: Application) throws { let adminRoutes = app.routes .grouped( AuthenticatedUser.redirectMiddleware( path: "/sign-in/" ) ) .grouped("admin") let _: [Void] = app.invokeAll( "admin-routes", args: ["routes": adminRoutes] ) } func adminRoutesHook(_ args: HookArguments) -> Void { let routes = args["routes"] as! RoutesBuilder routes.get(use: controller.dashboardView) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Admin/Controllers/AdminController.swift ================================================ import Vapor protocol AdminController: AdminListController, AdminDetailController, AdminCreateController, AdminUpdateController, AdminDeleteController { func setupRoutes( _ routes: RoutesBuilder ) } extension AdminController { func setupRoutes( _ routes: RoutesBuilder ) { setupListRoutes(routes) setupDetailRoutes(routes) setupCreateRoutes(routes) setupUpdateRoutes(routes) setupDeleteRoutes(routes) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Admin/Controllers/AdminCreateController.swift ================================================ import Vapor protocol AdminCreateController: CreateController { associatedtype CreateModelEditor: ModelEditorInterface func createTemplate( _ req: Request, _ editor: CreateModelEditor ) -> TemplateRepresentable func createView( _ req: Request ) async throws -> Response func createAction( _ req: Request ) async throws -> Response func createContext( _ req: Request, _ editor: CreateModelEditor ) -> AdminEditorPageContext func createBreadcrumbs( _ req: Request ) -> [LinkContext] func setupCreateRoutes( _ routes: RoutesBuilder ) } extension AdminCreateController { private func render( _ req: Request, editor: CreateModelEditor ) -> Response { req.templates.renderHtml( createTemplate(req, editor) ) } func createView( _ req: Request ) async throws -> Response { let editor = CreateModelEditor( model: .init(), form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) return render(req, editor: editor) } func createAction( _ req: Request ) async throws -> Response { let model = DatabaseModel() let editor = CreateModelEditor( model: model as! CreateModelEditor.Model, form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) try await editor.process(req: req) let isValid = try await editor.validate(req: req) guard isValid else { return render(req, editor: editor) } try await editor.write(req: req) try await create(req, editor.model as! DatabaseModel) try await editor.save(req: req) var components = req.url.path.pathComponents.dropLast() components += editor.model.id!.uuidString.pathComponents return req.redirect(to: "/" + components.string + "/update/") } func createTemplate( _ req: Request, _ editor: CreateModelEditor ) -> TemplateRepresentable { AdminEditorPageTemplate( createContext(req, editor) ) } func createContext( _ req: Request, _ editor: CreateModelEditor ) -> AdminEditorPageContext { let context = FormContext( action: editor.form.action, fields: editor.form.fields.map { $0.render(req: req) }, error: editor.form.error, submit: editor.form.submit ) return .init( title: "Create", form: context, breadcrumbs: createBreadcrumbs(req) ) } func createBreadcrumbs( _ req: Request ) -> [LinkContext] { [ LinkContext( label: Self.moduleName.capitalized, dropLast: 2 ), LinkContext( label: Self.modelName.plural.capitalized, dropLast: 1 ), ] } func setupCreateRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) baseRoutes.get("create", use: createView) baseRoutes.post("create", use: createAction) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Admin/Controllers/AdminDeleteController.swift ================================================ import Vapor final class DeleteForm: AbstractForm { init() { super.init() self.action.method = .post self.submit = "Delete" } } protocol AdminDeleteController: DeleteController { func deleteView( _ req: Request ) async throws -> Response func deleteAction( _ req: Request ) async throws -> Response func deleteTemplate( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> TemplateRepresentable func deleteInfo( _ model: DatabaseModel ) -> String func deleteContext( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> AdminDeletePageContext func setupDeleteRoutes( _ routes: RoutesBuilder ) } extension AdminDeleteController { func deleteView( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let form = DeleteForm() return req.templates.renderHtml( deleteTemplate(req, model, form) ) } func deleteAction( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) try await delete(req, model) var url = req.url.path if let redirect = try? req.query.get(String.self, at: "redirect") { url = redirect } return req.redirect(to: url) } func deleteTemplate( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> TemplateRepresentable { AdminDeletePageTemplate( deleteContext(req, model, form) ) } func deleteContext( _ req: Request, _ model: DatabaseModel, _ form: DeleteForm ) -> AdminDeletePageContext { .init( title: "Delete", name: deleteInfo(model), type: "model", form: form.getContext(req) ) } func setupDeleteRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.get("delete", use: deleteView) existingModelRoutes.post("delete", use: deleteAction) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Admin/Controllers/AdminDetailController.swift ================================================ import Vapor protocol AdminDetailController: DetailController { func detailView( _ req: Request ) async throws -> Response func detailTemplate( _ req: Request, _ model: DatabaseModel ) -> TemplateRepresentable func detailFields( for model: DatabaseModel ) -> [DetailContext] func detailContext( _ req: Request, _ model: DatabaseModel ) -> AdminDetailPageContext func detailBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func detailNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func setupDetailRoutes( _ routes: RoutesBuilder ) } extension AdminDetailController { func detailView( _ req: Request ) async throws -> Response { let model = try await detail(req) return req.templates.renderHtml(detailTemplate(req, model)) } func detailTemplate( _ req: Request, _ model: DatabaseModel ) -> TemplateRepresentable { AdminDetailPageTemplate(detailContext(req, model)) } func detailContext( _ req: Request, _ model: DatabaseModel ) -> AdminDetailPageContext { let path = "/delete/?redirect=" + req.url.path.pathComponents.dropLast().string + "&cancel=" + req.url.path return .init( title: "Details", fields: detailFields(for: model), navigation: detailNavigation(req, model), breadcrumbs: detailBreadcrumbs(req, model), actions: [ LinkContext( label: "Delete", path: path ), ] ) } func detailBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: Self.moduleName.capitalized, dropLast: 2 ), LinkContext( label: Self.modelName.plural.capitalized, dropLast: 1 ), ] } func detailNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: "Update", path: "update" ), ] } func setupDetailRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.get(use: detailView) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Admin/Controllers/AdminFrontendController.swift ================================================ import Vapor struct AdminFrontendController { func dashboardView( req: Request ) throws -> Response { let user = try req.auth.require(AuthenticatedUser.self) let template = AdminDashboardTemplate( .init( icon: "👋", title: "Dashboard", message: "Hello \(user.email), welcome to the CMS." ) ) return req.templates.renderHtml(template) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Admin/Controllers/AdminListController.swift ================================================ import Vapor protocol AdminListController: ListController { func listView( _ req: Request ) async throws -> Response func listColumns() -> [ColumnContext] func listCells( for model: DatabaseModel ) -> [CellContext] func listNavigation( _ req: Request ) -> [LinkContext] func listBreadcrumbs( _ req: Request ) -> [LinkContext] func listContext( _ req: Request, _ list: [DatabaseModel] ) -> AdminListPageContext func listTemplate( _ req: Request, _ list: [DatabaseModel] ) -> TemplateRepresentable func setupListRoutes( _ routes: RoutesBuilder ) } extension AdminListController { func listView(_ req: Request) async throws -> Response { let list = try await list(req) let template = listTemplate(req, list) return req.templates.renderHtml(template) } func listNavigation(_ req: Request) -> [LinkContext] { [ LinkContext(label: "Create",path: "create") ] } func listBreadcrumbs(_ req: Request) -> [LinkContext] { [ LinkContext( label: DatabaseModel.Module.identifier.capitalized, dropLast: 1 ) ] } func listContext( _ req: Request, _ list: [DatabaseModel] ) -> AdminListPageContext { let rows = list.map { RowContext(id: $0.id!.uuidString, cells: listCells(for: $0)) } let table = TableContext(columns: listColumns(), rows: rows, actions: [ LinkContext(label: "Update", path: "update"), LinkContext(label: "Delete", path: "delete") ]) return .init( title: "List", table: table, navigation: listNavigation(req), breadcrumbs: listBreadcrumbs(req) ) } func listTemplate( _ req: Request, _ list: [DatabaseModel] ) -> TemplateRepresentable { AdminListPageTemplate(listContext(req, list)) } func setupListRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) baseRoutes.get(use: listView) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Admin/Controllers/AdminUpdateController.swift ================================================ import Vapor protocol AdminUpdateController: UpdateController { associatedtype UpdateModelEditor: ModelEditorInterface func updateView( _ req: Request ) async throws -> Response func updateAction( _ req: Request ) async throws -> Response func updateTemplate( _ req: Request, _ editor: UpdateModelEditor ) async -> TemplateRepresentable func updateContext( _ req: Request, _ editor: UpdateModelEditor ) async -> AdminEditorPageContext func updateBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func updateNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] func setupUpdateRoutes( _ routes: RoutesBuilder ) } extension AdminUpdateController { private func render( _ req: Request, editor: UpdateModelEditor ) async -> Response { req.templates.renderHtml( await updateTemplate(req, editor) ) } func updateView( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let editor = UpdateModelEditor( model: model as! UpdateModelEditor.Model, form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) try await editor.read(req: req) return await render(req, editor: editor) } func updateAction( _ req: Request ) async throws -> Response { let model = try await findBy(identifier(req), on: req.db) let editor = UpdateModelEditor( model: model as! UpdateModelEditor.Model, form: .init() ) editor.form.fields = editor.formFields try await editor.load(req: req) try await editor.process(req: req) let isValid = try await editor.validate(req: req) guard isValid else { return await render(req, editor: editor) } try await editor.write(req: req) try await update(req, editor.model as! DatabaseModel) try await editor.save(req: req) return req.redirect(to: req.url.path) } func updateTemplate( _ req: Request, _ editor: UpdateModelEditor ) async -> TemplateRepresentable { await AdminEditorPageTemplate( updateContext(req, editor) ) } func updateContext( _ req: Request, _ editor: UpdateModelEditor ) async -> AdminEditorPageContext { let path = "delete/?redirect=" + req.url.path.pathComponents.dropLast(2).string + "&cancel=" + req.url.path let model = editor.model as! DatabaseModel return .init( title: "Update", form: editor.form.getContext(req), navigation: updateNavigation(req, model), breadcrumbs: updateBreadcrumbs(req, model), actions: [ LinkContext( label: "Delete", path: path, dropLast: 1 ), ] ) } func updateBreadcrumbs( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: Self.moduleName.capitalized, dropLast: 3 ), LinkContext( label: Self.modelName.plural.capitalized, dropLast: 2 ), ] } func updateNavigation( _ req: Request, _ model: DatabaseModel ) -> [LinkContext] { [ LinkContext( label: "Details", dropLast: 1 ), ] } func setupUpdateRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.get("update", use: updateView) existingModelRoutes.post("update", use: updateAction) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDashboardContext.swift ================================================ struct AdminDashboardContext { let icon: String let title: String let message: String } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDeletePageContext.swift ================================================ public struct AdminDeletePageContext { public let title: String public let name: String public let type: String public let form: FormContext public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public init( title: String, name: String, type: String, form: FormContext, navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [] ) { self.title = title self.name = name self.type = type self.form = form self.navigation = navigation self.breadcrumbs = breadcrumbs } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminDetailPageContext.swift ================================================ public struct AdminDetailPageContext { public let title: String public let fields: [DetailContext] public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public let actions: [LinkContext] public init( title: String, fields: [DetailContext], navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [], actions: [LinkContext] = [] ) { self.title = title self.fields = fields self.navigation = navigation self.breadcrumbs = breadcrumbs self.actions = actions } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminEditorPageContext.swift ================================================ public struct AdminEditorPageContext { public let title: String public let form: FormContext public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public let actions: [LinkContext] public init( title: String, form: FormContext, navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [], actions: [LinkContext] = [] ) { self.title = title self.form = form self.navigation = navigation self.breadcrumbs = breadcrumbs self.actions = actions } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminIndexContext.swift ================================================ public struct AdminIndexContext { public let title: String public let breadcrumbs: [LinkContext] public init( title: String, breadcrumbs: [LinkContext] = [] ) { self.title = title self.breadcrumbs = breadcrumbs } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Admin/Templates/Contexts/AdminListPageContext.swift ================================================ public struct AdminListPageContext { public let title: String public let table: TableContext public let navigation: [LinkContext] public let breadcrumbs: [LinkContext] public init( title: String, table: TableContext, navigation: [LinkContext] = [], breadcrumbs: [LinkContext] = [] ) { self.title = title self.table = table self.navigation = navigation self.breadcrumbs = breadcrumbs } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDashboardTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminDashboardTemplate: TemplateRepresentable { var context: AdminDashboardContext init( _ context: AdminDashboardContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } Div { let widgets: [TemplateRepresentable] = req.invokeAll("admin-widget") widgets.map { $0.render(req) } } .class("widgets") } .id("dashboard") .class("container") } .render(req) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDeletePageTemplate.swift ================================================ import Vapor import SwiftHtml public struct AdminDeletePageTemplate: TemplateRepresentable { var context: AdminDeletePageContext public init( _ context: AdminDeletePageContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { Span("🗑") .class("icon") H1(context.title) P("You are about to permanently delete the
`\(context.name)` \(context.type).") FormTemplate(context.form).render(req) A("Cancel") .href((try? req.query.get(String.self, at: "cancel")) ?? "#") .class(["button", "cancel"]) } .class(["lead", "container", "center"]) } .render(req) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Admin/Templates/Html/AdminDetailPageTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminDetailPageTemplate: TemplateRepresentable { var context: AdminDetailPageContext init( _ context: AdminDetailPageContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { Div { H1(context.title) for item in context.navigation { LinkTemplate(item).render(req) } } .class("lead") Dl { for item in context.fields { DetailTemplate(item).render(req) } } Section { for item in context.actions { LinkTemplate(item).render(req) } } } .class("container") } .render(req) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Admin/Templates/Html/AdminEditorPageTemplate.swift ================================================ import Vapor import SwiftHtml struct AdminEditorPageTemplate: TemplateRepresentable { var context: AdminEditorPageContext init( _ context: AdminEditorPageContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { Div { H1(context.title) for item in context.navigation { LinkTemplate(item).render(req) } } .class("lead") FormTemplate(context.form).render(req) Section { for item in context.actions { LinkTemplate(item).render(req) } } } .class("container") } .render(req) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Admin/Templates/Html/AdminIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg public struct AdminIndexTemplate: TemplateRepresentable { public var context: AdminIndexContext var body: Tag public init( _ context: AdminIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Meta() .name("robots") .content("noindex") Link(rel: .shortcutIcon) .href("/images/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css") Link(rel: .stylesheet) .href("/css/admin.css") Title(context.title) } Body { Div { A { Img(src: "/img/logo.png", alt: "Logo") .title("Logo") .style("width: 300px") } .href("/") Nav { Input() .type(.checkbox) .id("secondary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("secondary-menu-button") Div { A("Sign out") .href("/sign-out/") } .class("menu-items") } .id("secondary-menu") } .id("navigation") Div { Nav { A("Admin") .href("/admin/") for breadcrumb in context.breadcrumbs { LinkTemplate(breadcrumb).render(req) } } } .class("breadcrumb") Main { body } Script() .type(.javascript) .src("/js/admin.js") } } .lang("en-US") } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Admin/Templates/Html/AdminListPageTemplate.swift ================================================ import Vapor import SwiftHtml public struct AdminListPageTemplate: TemplateRepresentable { var context: AdminListPageContext public init( _ context: AdminListPageContext ) { self.context = context } @TagBuilder public func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init( title: context.title, breadcrumbs: context.breadcrumbs ) ) { Div { H1(context.title) P { context.navigation.map { LinkTemplate($0).render(req) } } } .class("lead") if context.table.rows.isEmpty { Div { Span("🔍") .class("icon") H2("Oh no") P("This list is empty right now.") A("Try again →") .href(req.url.path) .class("button-1") } .class(["lead", "container", "center"]) } else { TableTemplate(context.table).render(req) } } .render(req) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Api/ApiModule.swift ================================================ import Vapor struct ApiModule: ModuleInterface { let router = ApiRouter() func boot(_ app: Application) throws { app.middleware.use(ApiErrorMiddleware()) try router.boot(routes: app.routes) } func setUp(_ app: Application) throws { try router.setUpRoutesHooks(app: app) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Api/ApiRouter.swift ================================================ import Vapor struct ApiRouter: RouteCollection { func boot(routes: RoutesBuilder) throws {} func setUpRoutesHooks(app: Application) throws { let apiRoutes = app.routes .grouped(AuthenticatedUser.guardMiddleware()) .grouped("api") let _: [Void] = app.invokeAll( "api-routes", args: ["routes": apiRoutes] ) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Api/Controllers/ApiController.swift ================================================ import Vapor protocol ApiController: ApiListController, ApiDetailController, ApiCreateController, ApiUpdateController, ApiPatchController, ApiDeleteController { func validators( optional: Bool ) -> [AsyncValidator] func setupRoutes( _ routes: RoutesBuilder ) } extension ApiController { func validators( optional: Bool ) -> [AsyncValidator] { [] } func createValidators() -> [AsyncValidator] { validators(optional: false) } func updateValidators() -> [AsyncValidator] { validators(optional: false) } func patchValidators() -> [AsyncValidator] { validators(optional: true) } func createResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response { try await detailOutput(req, model) .encodeResponse(status: .created, for: req) } func updateResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response { try await detailOutput(req, model) .encodeResponse(for: req) } func patchResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response { try await detailOutput(req, model) .encodeResponse(for: req) } func setupRoutes( _ routes: RoutesBuilder ) { setupListRoutes(routes) setupDetailRoutes(routes) setupCreateRoutes(routes) setupUpdateRoutes(routes) setupPatchRoutes(routes) setupDeleteRoutes(routes) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Api/Controllers/ApiCreateController.swift ================================================ import Vapor protocol ApiCreateController: CreateController { associatedtype CreateObject: Decodable func createValidators() -> [AsyncValidator] func createInput( _ req: Request, _ model: DatabaseModel, _ input: CreateObject ) async throws func createApi( _ req: Request ) async throws -> Response func createResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response func setupCreateRoutes( _ routes: RoutesBuilder ) } extension ApiCreateController { func createValidators() -> [AsyncValidator] { [] } func createApi( _ req: Request ) async throws -> Response { try await RequestValidator(createValidators()).validate(req) let input = try req.content.decode(CreateObject.self) let model = DatabaseModel() try await createInput(req, model, input) try await create(req, model) return try await createResponse(req, model) } func setupCreateRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) baseRoutes.post(use: createApi) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Api/Controllers/ApiDeleteController.swift ================================================ import Vapor public protocol ApiDeleteController: DeleteController { func deleteApi( _ req: Request ) async throws -> HTTPStatus func setupDeleteRoutes( _ routes: RoutesBuilder ) } public extension ApiDeleteController { func deleteApi( _ req: Request ) async throws -> HTTPStatus { let model = try await findBy(identifier(req), on: req.db) try await delete(req, model) return .noContent } func setupDeleteRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.delete(use: deleteApi) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Api/Controllers/ApiDetailController.swift ================================================ import Vapor protocol ApiDetailController: DetailController { associatedtype DetailObject: Content func detailOutput( _ req: Request, _ model: DatabaseModel ) async throws -> DetailObject func detailApi( _ req: Request ) async throws -> DetailObject func setupDetailRoutes( _ routes: RoutesBuilder ) } extension ApiDetailController { func detailApi( _ req: Request ) async throws -> DetailObject { let model = try await detail(req) return try await detailOutput(req, model) } func setupDetailRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.get(use: detailApi) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Api/Controllers/ApiListController.swift ================================================ import Vapor protocol ApiListController: ListController { associatedtype ListObject: Content func listOutput( _ req: Request, _ models: [DatabaseModel] ) async throws -> [ListObject] func listApi( _ req: Request ) async throws -> [ListObject] func setupListRoutes( _ routes: RoutesBuilder ) } extension ApiListController { func listApi( _ req: Request ) async throws -> [ListObject] { let models = try await list(req) return try await listOutput(req, models) } func setupListRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) baseRoutes.get(use: listApi) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Api/Controllers/ApiPatchController.swift ================================================ import Vapor public protocol ApiPatchController: PatchController { associatedtype PatchObject: Decodable func patchValidators() -> [AsyncValidator] func patchInput( _ req: Request, _ model: DatabaseModel, _ input: PatchObject ) async throws func patchApi( _ req: Request ) async throws -> Response func patchResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response func setupPatchRoutes( _ routes: RoutesBuilder ) } public extension ApiPatchController { func patchValidators() -> [AsyncValidator] { [] } func patchApi( _ req: Request ) async throws -> Response { try await RequestValidator(patchValidators()).validate(req) let model = try await findBy(identifier(req), on: req.db) let input = try req.content.decode(PatchObject.self) try await patchInput(req, model, input) try await patch(req, model) return try await patchResponse(req, model) } func setupPatchRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.patch(use: patchApi) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Api/Controllers/ApiUpdateController.swift ================================================ import Vapor public protocol ApiUpdateController: UpdateController { associatedtype UpdateObject: Decodable func updateValidators() -> [AsyncValidator] func updateInput( _ req: Request, _ model: DatabaseModel, _ input: UpdateObject ) async throws func updateApi( _ req: Request ) async throws -> Response func updateResponse( _ req: Request, _ model: DatabaseModel ) async throws -> Response func setupUpdateRoutes( _ routes: RoutesBuilder ) } public extension ApiUpdateController { func updateValidators() -> [AsyncValidator] { [] } func updateApi( _ req: Request ) async throws -> Response { try await RequestValidator(updateValidators()).validate(req) let model = try await findBy(identifier(req), on: req.db) let input = try req.content.decode(UpdateObject.self) try await updateInput(req, model, input) try await update(req, model) return try await updateResponse(req, model) } func setupUpdateRoutes( _ routes: RoutesBuilder ) { let baseRoutes = getBaseRoutes(routes) let existingModelRoutes = baseRoutes .grouped(ApiModel.pathIdComponent) existingModelRoutes.put(use: updateApi) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Api/Middlewares/ApiErrorMiddleware.swift ================================================ import Vapor struct ApiErrorMiddleware: AsyncMiddleware { func respond( to req: Request, chainingTo next: AsyncResponder ) async throws -> Response { do { return try await next.respond(to: req) } catch { let status: HTTPResponseStatus let headers: HTTPHeaders let message: String? let details: [ValidationErrorDetail] switch error { case let abort as ValidationAbort: status = abort.abort.status headers = abort.abort.headers message = abort.message details = abort.details case let abort as Abort: status = abort.status headers = abort.headers message = abort.reason details = [] default: status = .internalServerError headers = [:] if req.application.environment.isRelease { message = "Something went wrong." } else { message = error.localizedDescription } details = [] } req.logger.report(error: error) let response = Response( status: status, headers: headers ) do { let encoder = JSONEncoder() let data = try encoder.encode( ValidationError( message: message, details: details ) ) response.body = .init(data: data) response.headers.replaceOrAdd( name: .contentType, value: "application/json; charset=utf-8" ) } catch { response.body = .init(string: "Oops: \(error)") response.headers.replaceOrAdd( name: .contentType, value: "text/plain; charset=utf-8" ) } return response } } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Blog/BlogModule.swift ================================================ import Vapor struct BlogModule: ModuleInterface { let router = BlogRouter() func boot(_ app: Application) throws { app.migrations.add(BlogMigrations.v1()) app.migrations.add(BlogMigrations.seed()) app.hooks.register("admin-widget", use: adminWidgetHook) app.hooks.register("admin-routes", use: router.adminRoutesHook) app.hooks.register("api-routes", use: router.apiRoutesHook) app.hooks.registerAsync("response", use: router.responseHook) try router.boot(routes: app.routes) } func adminWidgetHook( _ args: HookArguments ) -> TemplateRepresentable { BlogAdminWidgetTemplate() } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Blog/BlogRouter.swift ================================================ import Vapor struct BlogRouter: RouteCollection { let frontendController = BlogFrontendController() let postAdminController = BlogPostAdminController() let postApiController = BlogPostApiController() let categoryAdminController = BlogCategoryAdminController() let categoryApiController = BlogCategoryApiController() func boot(routes: RoutesBuilder) throws { routes.get("blog", use: frontendController.blogView) } func adminRoutesHook(_ args: HookArguments) -> Void { let routes = args["routes"] as! RoutesBuilder postAdminController.setupRoutes(routes) categoryAdminController.setupRoutes(routes) } func apiRoutesHook(_ args: HookArguments) -> Void { let routes = args["routes"] as! RoutesBuilder postApiController.setupRoutes(routes) categoryApiController.setupRoutes(routes) } func responseHook( _ args: HookArguments ) async throws -> Response? { let req = args["req"] as! Request return try await frontendController.postView(req) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Blog/Controllers/BlogCategoryAdminController.swift ================================================ import Vapor import Fluent struct BlogCategoryAdminController: AdminController { typealias ApiModel = Blog.Category typealias DatabaseModel = BlogCategoryModel typealias CreateModelEditor = BlogCategoryEditor typealias UpdateModelEditor = BlogCategoryEditor func listColumns() -> [ColumnContext] { [ .init("title"), ] } func listCells( for model: DatabaseModel ) -> [CellContext] { [ .init( model.title, link: .init(label: model.title) ), ] } func detailFields( for model: DatabaseModel ) -> [DetailContext] { [ .init("title", model.title), ] } func deleteInfo( _ model: DatabaseModel ) -> String { model.title } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Blog/Controllers/BlogCategoryApiController.swift ================================================ import Vapor extension Blog.Category.List: Content {} extension Blog.Category.Detail: Content {} struct BlogCategoryApiController: ApiController { typealias ApiModel = Blog.Category typealias DatabaseModel = BlogCategoryModel @AsyncValidatorBuilder func validators( optional: Bool ) -> [AsyncValidator] { KeyedContentValidator.required("title", optional: optional) } func listOutput( _ req: Request, _ models: [BlogCategoryModel] ) async throws -> [Blog.Category.List] { models.map { .init(id: $0.id!, title: $0.title) } } func detailOutput( _ req: Request, _ model: BlogCategoryModel ) async throws -> Blog.Category.Detail { .init( id: model.id!, title: model.title ) } func createInput( _ req: Request, _ model: BlogCategoryModel, _ input: Blog.Category.Create ) async throws { model.title = input.title } func updateInput( _ req: Request, _ model: BlogCategoryModel, _ input: Blog.Category.Update ) async throws { model.title = input.title } func patchInput( _ req: Request, _ model: BlogCategoryModel, _ input: Blog.Category.Patch ) async throws { model.title = input.title ?? model.title } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift ================================================ import Vapor import Fluent struct BlogFrontendController { func blogView(req: Request) async throws -> Response { let posts = try await BlogPostModel .query(on: req.db) .sort(\.$date, .descending) .all() let api = BlogPostApiController() let listOutput = try await api.listOutput(req, posts) let ctx = BlogPostsContext( icon: "🔥", title: "Blog", message: "Hot news and stories about everything.", posts: listOutput ) return req.templates.renderHtml(BlogPostsTemplate(ctx)) } func postView( _ req: Request ) async throws -> Response? { let slug = req.url.path.trimmingCharacters( in: .init(charactersIn: "/") ) guard let post = try await BlogPostModel .query(on: req.db) .filter(\.$slug == slug) .first() else { return nil } let model = try await BlogPostApiController().detailOutput(req, post) let context = BlogPostContext(post: model) return req.templates.renderHtml(BlogPostTemplate(context)) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Blog/Controllers/BlogPostAdminController.swift ================================================ import Vapor import Fluent struct BlogPostAdminController: AdminController { typealias ApiModel = Blog.Post typealias DatabaseModel = BlogPostModel typealias CreateModelEditor = BlogPostEditor typealias UpdateModelEditor = BlogPostEditor func listColumns() -> [ColumnContext] { [ .init("image"), .init("title"), ] } func listCells(for model: DatabaseModel) -> [CellContext] { [ .init(model.imageKey, type: .image), .init(model.title, link: .init(label: model.title)), ] } func detailFields(for model: DatabaseModel) -> [DetailContext] { [ .init("image", model.imageKey, type: .image), .init("title", model.title), ] } func deleteInfo( _ model: DatabaseModel ) -> String { model.title } func beforeDelete( _ req: Request, _ model: BlogPostModel ) async throws { try await req.fs.delete(key: model.imageKey) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Blog/Controllers/BlogPostApiController.swift ================================================ import Vapor extension Blog.Post.List: Content {} extension Blog.Post.Detail: Content {} struct BlogPostApiController: ApiController { typealias ApiModel = Blog.Post typealias DatabaseModel = BlogPostModel @AsyncValidatorBuilder func validators(optional: Bool) -> [AsyncValidator] { KeyedContentValidator.required("title", optional: optional) KeyedContentValidator.required("slug", optional: optional) KeyedContentValidator.required("image", optional: optional) KeyedContentValidator.required("excerpt", optional: optional) KeyedContentValidator.required("content", optional: optional) KeyedContentValidator.required("categoryId", optional: optional) KeyedContentValidator("categoryId", "Invalid or missing category", optional: optional) { value, req in try await BlogCategoryModel.find(value, on: req.db) != nil } } func listOutput( _ req: Request, _ models: [DatabaseModel] ) async throws -> [Blog.Post.List] { models.map { model in .init( id: model.id!, title: model.title, slug: model.slug, image: model.imageKey, excerpt: model.excerpt, date: model.date ) } } func detailOutput( _ req: Request, _ model: DatabaseModel ) async throws -> Blog.Post.Detail { guard let categoryModel = try await BlogCategoryModel .find(model.$category.id, on: req.db), let category = try await BlogCategoryApiController() .listOutput(req, [categoryModel]) .first else { throw Abort(.internalServerError) } return .init( id: model.id!, title: model.title, slug: model.slug, image: model.imageKey, excerpt: model.excerpt, date: model.date, category: category, content: model.content ) } func createInput( _ req: Request, _ model: DatabaseModel, _ input: Blog.Post.Create ) async throws { model.title = input.title model.slug = input.slug model.imageKey = input.image model.excerpt = input.excerpt model.date = input.date model.content = input.content model.$category.id = input.categoryId } func updateInput( _ req: Request, _ model: DatabaseModel, _ input: Blog.Post.Update ) async throws { model.title = input.title model.slug = input.slug model.imageKey = input.image model.excerpt = input.excerpt model.date = input.date model.content = input.content model.$category.id = input.categoryId } func patchInput( _ req: Request, _ model: DatabaseModel, _ input: Blog.Post.Patch ) async throws { model.title = input.title ?? model.title model.slug = input.slug ?? model.slug model.imageKey = input.image ?? model.imageKey model.excerpt = input.excerpt ?? model.excerpt model.date = input.date ?? model.date model.content = input.content ?? model.content model.$category.id = input.categoryId ?? model.$category.id } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Blog/Database/Migrations/BlogMigrations.swift ================================================ import Foundation import Fluent enum BlogMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema) .id() .field(BlogCategoryModel.FieldKeys.v1.title, .string, .required) .create() try await db.schema(BlogPostModel.schema) .id() .field(BlogPostModel.FieldKeys.v1.title, .string, .required) .field(BlogPostModel.FieldKeys.v1.slug, .string, .required) .field(BlogPostModel.FieldKeys.v1.imageKey, .string, .required) .field(BlogPostModel.FieldKeys.v1.excerpt, .data, .required) .field(BlogPostModel.FieldKeys.v1.date, .datetime, .required) .field(BlogPostModel.FieldKeys.v1.content, .data, .required) .field(BlogPostModel.FieldKeys.v1.categoryId, .uuid) .foreignKey( BlogPostModel.FieldKeys.v1.categoryId, references: BlogCategoryModel.schema, .id, onDelete: DatabaseSchema.ForeignKeyAction.setNull, onUpdate: .cascade ) .unique(on: BlogPostModel.FieldKeys.v1.slug) .create() } func revert(on db: Database) async throws { try await db.schema(BlogCategoryModel.schema).delete() try await db.schema(BlogPostModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let categories = (1...4).map { index in BlogCategoryModel(title: "Sample category #\(index)") } try await categories.create(on: db) try await (1...9).map { index in BlogPostModel( id: nil, title: "Sample post #\(index)", slug: "sample-post-\(index)", imageKey: "/img/posts/\(String(format: "%02d", index + 1)).jpg", excerpt: "Lorem ipsum", date: Date().addingTimeInterval(-Double.random(in: 0...(86400 * 60))), content: "Lorem ipsum dolor sit amet.", categoryId: categories[Int.random(in: 0.. [FormComponent] { ImageField("image", path: "blog/post") .read { [unowned self] in $1.output.context.previewUrl = model.imageKey ($1 as! ImageField).imageKey = model.imageKey } .write { [unowned self] in model.imageKey = ($1 as! ImageField).imageKey ?? "" } InputField("slug") .config { $0.output.context.label.required = true } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = model.slug } .write { [unowned self] in model.slug = $1.input } InputField("title") .config { $0.output.context.label.required = true } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = model.title } .write { [unowned self] in model.title = $1.input } InputField("date") .config { $0.output.context.label.required = true $0.output.context.value = dateFormatter.string(from: Date()) } .validators { FormFieldValidator.required($1) } .read { [unowned self] in $1.output.context.value = dateFormatter.string(from: model.date) } .write { [unowned self] in model.date = dateFormatter.date(from: $1.input) ?? Date() } TextareaField("excerpt") .read { [unowned self] in $1.output.context.value = model.excerpt } .write { [unowned self] in model.excerpt = $1.input } TextareaField("content") .read { [unowned self] in $1.output.context.value = model.content } .write { [unowned self] in model.content = $1.input } SelectField("category") .load { req, field in let categories = try await BlogCategoryModel .query(on: req.db) .all() field.output.context.options = categories.map { OptionContext(key: $0.id!.uuidString, label: $0.title) } } .read { [unowned self] req, field in field.output.context.value = model.$category.id.uuidString } .write { [unowned self] req, field in if let uuid = UUID(uuidString: field.input), let category = try await BlogCategoryModel .find(uuid, on: req.db) { model.$category.id = category.id! } } } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminDeleteContext.swift ================================================ struct BlogPostAdminDeleteContext { let title: String let name: String let type: String } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminDetailContext.swift ================================================ struct BlogPostAdminDetailContext { let title: String let detail: Blog.Post.Detail } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminEditContext.swift ================================================ struct BlogPostAdminEditContext { let title: String let form: TemplateRepresentable } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostAdminListContext.swift ================================================ struct BlogPostAdminListContext { let title: String let list: [Blog.Post.List] } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostContext.swift ================================================ struct BlogPostContext { let post: Blog.Post.Detail } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostsContext.swift ================================================ struct BlogPostsContext { let icon: String let title: String let message: String let posts: [Blog.Post.List] } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Blog/Templates/Html/BlogAdminWidgetTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogAdminWidgetTemplate: TemplateRepresentable { @TagBuilder func render(_ req: Request) -> Tag { H2("Blog") Ul { Li { A("Posts") .href("/admin/blog/posts/") } Li { A("Categories") .href("/admin/blog/categories/") } } } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminDeleteTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminDeleteTemplate: TemplateRepresentable { var context: BlogPostAdminDeleteContext init( _ context: BlogPostAdminDeleteContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Span("🗑") .class("icon") H1(context.title) P("You are about to permanently delete the
`\(context.name)` \(context.type).") Form { Input() .type(.submit) .class(["button", "destructive"]) .style("display: inline") .value("Delete") A("Cancel") .href("/admin/blog/posts/") .class(["button", "cancel"]) } .method(.post) .id("delete-form") } .class(["lead", "container", "center"]) } .render(req) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminDetailTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminDetailTemplate: TemplateRepresentable { var context: BlogPostAdminDetailContext init( _ context: BlogPostAdminDetailContext ) { self.context = context } var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .short return formatter }() @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") Dl { Dt("Image") Dd { Img( src: context.detail.image, alt: context.detail.title ) } Dt("Title") Dd(context.detail.title) Dt("Excerpt") Dd(context.detail.excerpt) Dt("Date") Dd(dateFormatter.string(from: context.detail.date)) Dt("Content") Dd(context.detail.content) } } .id("detail") .class("container") } .render(req) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminEditTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminEditTemplate: TemplateRepresentable { var context: BlogPostAdminEditContext init( _ context: BlogPostAdminEditContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") context.form.render(req) } .id("edit") .class("container") } .render(req) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostAdminListTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostAdminListTemplate: TemplateRepresentable { var context: BlogPostAdminListContext init( _ context: BlogPostAdminListContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { AdminIndexTemplate( .init(title: context.title) ) { Div { Section { H1(context.title) } .class("lead") Table { Thead { Tr { Th("Image") Th("Title") Th("Preview") } } Tbody { for item in context.list { Tr { Td { Img(src: item.image, alt: item.title) } Td { A(item.title) .href("/admin/blog/posts/" + item.id.uuidString + "/") } Td { A("Preview") .href("/" + item.slug + "/") } } } } } } .id("list") } .render(req) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostTemplate: TemplateRepresentable { var context: BlogPostContext init( _ context: BlogPostContext ) { self.context = context } var dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeStyle = .short return formatter }() @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.post.title) ) { Div { Section { P(dateFormatter.string(from: context.post.date)) H1(context.post.title) P(context.post.excerpt) } .class(["lead", "container"]) Img(src: context.post.image, alt: context.post.title) Article { Text(context.post.content) } .class("container") } .id("post") } .render(req) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostsTemplate.swift ================================================ import Vapor import SwiftHtml struct BlogPostsTemplate: TemplateRepresentable { var context: BlogPostsContext init( _ context: BlogPostsContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") Div { for post in context.posts { Article { A { Img(src: post.image, alt: post.title) H2(post.title) P(post.excerpt) } .href("/\(post.slug)/") } } } .class("grid-221") } .id("blog") } .render(req) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/User/Authenticators/UserCredentialsAuthenticator.swift ================================================ import Vapor import Fluent struct UserCredentialsAuthenticator: AsyncCredentialsAuthenticator { struct Credentials: Content { let email: String let password: String } func authenticate( credentials: Credentials, for req: Request ) async throws { guard let user = try await UserAccountModel .query(on: req.db) .filter(\.$email == credentials.email) .first() else { return } do { guard try Bcrypt.verify( credentials.password, created: user.password ) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } catch { // do nothing... } } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/User/Authenticators/UserSessionAuthenticator.swift ================================================ import Vapor import Fluent struct UserSessionAuthenticator: AsyncSessionAuthenticator { typealias User = AuthenticatedUser func authenticate( sessionID: User.SessionID, for req: Request ) async throws { guard let user = try await UserAccountModel .find(sessionID, on: req.db) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/User/Authenticators/UserTokenAuthenticator.swift ================================================ import Vapor import Fluent struct UserTokenAuthenticator: AsyncBearerAuthenticator { func authenticate( bearer: BearerAuthorization, for req: Request ) async throws { guard let token = try await UserTokenModel .query(on: req.db) .filter(\.$value == bearer.token) .first() else { return } guard let user = try await UserAccountModel .find(token.$user.id, on: req.db) else { return } req.auth.login( AuthenticatedUser( id: user.id!, email: user.email ) ) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/User/Controllers/UserApiController.swift ================================================ import Vapor extension User.Token.Detail: Content {} struct UserApiController { func signInApi( req: Request ) async throws -> User.Token.Detail { guard let user = req.auth.get(AuthenticatedUser.self) else { throw Abort(.unauthorized) } let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789=" let tokenValue = String((0..<64).map { _ in letters.randomElement()! }) let token = UserTokenModel( value: tokenValue, userId: user.id ) try await token.create(on: req.db) let account = User.Account.Detail( id: user.id, email: user.email ) return .init( id: token.id!, value: token.value, user: account ) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/User/Controllers/UserFrontendController.swift ================================================ import Vapor struct UserFrontendController { private func renderSignInView( _ req: Request, _ form: UserLoginForm ) -> Response { let template = UserLoginTemplate( .init( icon: "⬇️", title: "Sign in", message: "Please log in with your existing account", form: form.render(req: req) ) ) return req.templates.renderHtml(template) } func signInView( _ req: Request ) async throws -> Response { renderSignInView(req, .init()) } func signInAction( _ req: Request ) async throws -> Response { /// the user is authenticated, we can store the user data inside the session too if let user = req.auth.get(AuthenticatedUser.self) { req.session.authenticate(user) return req.redirect(to: "/") } let form = UserLoginForm() try await form.process(req: req) let isValid = try await form.validate(req: req) if !isValid { form.error = "Invalid email or password." } return renderSignInView(req, form) } func signOut( _ req: Request ) throws -> Response { req.auth.logout(AuthenticatedUser.self) req.session.unauthenticate(AuthenticatedUser.self) return req.redirect(to: "/") } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/User/Database/Migrations/UserMigrations.swift ================================================ import Vapor import Fluent enum UserMigrations { struct v1: AsyncMigration { func prepare(on db: Database) async throws { try await db.schema(UserAccountModel.schema) .id() .field(UserAccountModel.FieldKeys.v1.email, .string, .required) .field(UserAccountModel.FieldKeys.v1.password, .string, .required) .unique(on: UserAccountModel.FieldKeys.v1.email) .create() try await db.schema(UserTokenModel.schema) .id() .field(UserTokenModel.FieldKeys.v1.value, .string, .required) .field(UserTokenModel.FieldKeys.v1.userId, .uuid, .required) .foreignKey( UserTokenModel.FieldKeys.v1.userId, references: UserAccountModel.schema, .id ) .unique(on: UserTokenModel.FieldKeys.v1.value) .create() } func revert(on db: Database) async throws { try await db.schema(UserTokenModel.schema).delete() try await db.schema(UserAccountModel.schema).delete() } } struct seed: AsyncMigration { func prepare(on db: Database) async throws { let email = "root@localhost.com" let password = "ChangeMe1" let user = UserAccountModel( email: email, password: try Bcrypt.hash(password) ) try await user.create(on: db) } func revert(on db: Database) async throws { try await UserAccountModel.query(on: db).delete() } } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/User/Database/Models/UserAccountModel.swift ================================================ import Vapor import Fluent final class UserAccountModel: DatabaseModelInterface { typealias Module = UserModule struct FieldKeys { struct v1 { static var email: FieldKey { "email" } static var password: FieldKey { "password" } } } @ID() var id: UUID? @Field(key: FieldKeys.v1.email) var email: String @Field(key: FieldKeys.v1.password) var password: String init() { } init( id: UUID? = nil, email: String, password: String ) { self.id = id self.email = email self.password = password } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/User/Database/Models/UserTokenModel.swift ================================================ import Vapor import Fluent final class UserTokenModel: DatabaseModelInterface { typealias Module = UserModule struct FieldKeys { struct v1 { static var value: FieldKey { "value" } static var userId: FieldKey { "user_id" } } } @ID() var id: UUID? @Field(key: FieldKeys.v1.value) var value: String @Parent(key: FieldKeys.v1.userId) var user: UserAccountModel init() { } init( id: UUID? = nil, value: String, userId: UUID ) { self.id = id self.value = value self.$user.id = userId } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/User/Forms/UserLoginForm.swift ================================================ import Vapor final class UserLoginForm: AbstractForm { public convenience init() { self.init( action: .init( method: .post, url: "/sign-in/" ), submit: "Sign in" ) self.fields = createFields() } @FormComponentBuilder func createFields() -> [FormComponent] { InputField("email") .config { $0.output.context.label.required = true $0.output.context.type = .email } .validators { FormFieldValidator.required($1) FormFieldValidator.email($1) } InputField("password") .config { $0.output.context.label.required = true $0.output.context.type = .password } .validators { FormFieldValidator.required($1) } } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/User/Templates/Contexts/UserLoginContext.swift ================================================ struct UserLoginContext { let icon: String let title: String let message: String let form: TemplateRepresentable init( icon: String, title: String, message: String, form: TemplateRepresentable ) { self.icon = icon self.title = title self.message = message self.form = form } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/User/Templates/Html/UserLoginTemplate.swift ================================================ import Vapor import SwiftHtml struct UserLoginTemplate: TemplateRepresentable { var context: UserLoginContext init( _ context: UserLoginContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") context.form.render(req) } .id("user-login") .class("container") } .render(req) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/User/UserModule.swift ================================================ import Vapor struct UserModule: ModuleInterface { let router = UserRouter() func boot(_ app: Application) throws { app.migrations.add(UserMigrations.v1()) app.migrations.add(UserMigrations.seed()) app.middleware.use(UserSessionAuthenticator()) app.middleware.use(UserTokenAuthenticator()) try router.boot(routes: app.routes) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/User/UserRouter.swift ================================================ import Vapor struct UserRouter: RouteCollection { let frontendController = UserFrontendController() let apiController = UserApiController() func boot(routes: RoutesBuilder) throws { routes.get("sign-in", use: frontendController.signInView) routes .grouped(UserCredentialsAuthenticator()) .post("sign-in", use: frontendController.signInAction) routes.get("sign-out", use: frontendController.signOut) routes.grouped("api") .grouped(UserCredentialsAuthenticator()) .post("sign-in", use: apiController.signInApi) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Web/Controllers/WebFrontendController.swift ================================================ import Vapor struct WebFrontendController { func anyResponse( _ req: Request ) async throws -> Response { let result: [Response?] = try await req.invokeAllAsync("response") guard let response = result.compactMap({ $0 }).first else { throw Abort(.notFound) } return response } func homeView(req: Request) throws -> Response { let ctx = WebHomeContext( icon: "👋", title: "Home", message: "Hi there, welcome to my page.", paragraphs: [ "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.", "Nisi ut aliquip ex ea commodo consequat.", ], link: .init( label: "Read my blog →", url: "/blog/" ) ) return req.templates.renderHtml( WebHomeTemplate(ctx) ) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Web/Templates/Contexts/WebHomeContext.swift ================================================ struct WebHomeContext { let icon: String let title: String let message: String let paragraphs: [String] let link: WebLinkContext } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Web/Templates/Contexts/WebIndexContext.swift ================================================ public struct WebIndexContext { public let title: String public init( title: String ) { self.title = title } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Web/Templates/Contexts/WebLinkContext.swift ================================================ public struct WebLinkContext { public let label: String public let url: String public init( label: String, url: String ) { self.label = label self.url = url } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Web/Templates/Html/WebHomeTemplate.swift ================================================ import Vapor import SwiftHtml struct WebHomeTemplate: TemplateRepresentable { var context: WebHomeContext init( _ context: WebHomeContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { WebIndexTemplate( .init(title: context.title) ) { Div { Section { P(context.icon) H1(context.title) P(context.message) } .class("lead") for paragraph in context.paragraphs { P(paragraph) } WebLinkTemplate(context.link).render(req) } .id("home") .class("container") } .render(req) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift ================================================ import Vapor import SwiftHtml import SwiftSvg public struct WebIndexTemplate: TemplateRepresentable { public var context: WebIndexContext var body: Tag public init( _ context: WebIndexContext, @TagBuilder _ builder: () -> Tag ) { self.context = context self.body = builder() } @TagBuilder public func render( _ req: Request ) -> Tag { Html { Head { Meta() .charset("utf-8") Meta() .name(.viewport) .content("width=device-width, initial-scale=1") Link(rel: .shortcutIcon) .href("/images/favicon.ico") .type("image/x-icon") Link(rel: .stylesheet) .href("https://cdn.jsdelivr.net/gh/feathercms/feather-core@1.0.0-beta.44/feather.min.css") Link(rel: .stylesheet) .href("/css/web.css") Title(context.title) } Body { Header { Div { A { Img(src: "/img/logo.png", alt: "Logo") } .id("site-logo") .href("/") Nav { Input() .type(.checkbox) .id("primary-menu-button") .name("menu-button") .class("menu-button") Label { Svg.menuIcon() } .for("primary-menu-button") Div { A("Home") .href("/") .class("selected", req.url.path == "/") A("Blog") .href("/blog/") .class("selected", req.url.path == "/blog/") A("About") .href("#") .onClick("javascript:about();") if req.auth.has(AuthenticatedUser.self) { A("Admin") .href("/admin/") A("Sign out") .href("/sign-out/") } else { A("Sign in") .href("/sign-in/") } } .class("menu-items") } .id("primary-menu") } .id("navigation") } Main { body } Footer { Section { P { Text("This site is powered by ") A("Swift") .href("https://swift.org") .target(.blank) Text(" & ") A("Vapor") .href("https://vapor.codes") .target(.blank) Text(".") } P("myPage © 2020-2022") } } Script() .type(.javascript) .src("/js/web.js") } } .lang("en-US") } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Web/Templates/Html/WebLinkTemplate.swift ================================================ import Vapor import SwiftHtml struct WebLinkTemplate: TemplateRepresentable { var context: WebLinkContext init( _ context: WebLinkContext ) { self.context = context } @TagBuilder func render( _ req: Request ) -> Tag { A(context.label) .href(context.url) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Web/WebModule.swift ================================================ import Vapor struct WebModule: ModuleInterface { let router = WebRouter() func boot(_ app: Application) throws { try router.boot(routes: app.routes) } func setUp(_ app: Application) throws { try router.setUpRoutesHooks(app: app) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Modules/Web/WebRouter.swift ================================================ import Vapor struct WebRouter: RouteCollection { let controller = WebFrontendController() func boot(routes: RoutesBuilder) throws { routes.get(use: controller.homeView) } func setUpRoutesHooks(app: Application) throws { app.routes.get(.anything, use: controller.anyResponse) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Template/Request+Template.swift ================================================ import Vapor public extension Request { var templates: TemplateRenderer { .init(self) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Template/TemplateRenderer.swift ================================================ import Vapor import SwiftHtml public struct TemplateRenderer { var req: Request init(_ req: Request) { self.req = req } public func renderHtml( _ template: TemplateRepresentable, minify: Bool = false, indent: Int = 4 ) -> Response { let doc = Document(.html) { template.render(req) } let body = DocumentRenderer( minify: minify, indent: indent ) .render(doc) return Response( status: .ok, headers: [ "content-type": "text/html" ], body: .init(string: body) ) } } ================================================ FILE: Chapter 16/myProject/Sources/App/Template/TemplateRepresentable.swift ================================================ /// FILE: Sources/App/Template/TemplateRepresentable.swift import Vapor import SwiftSgml public protocol TemplateRepresentable { @TagBuilder func render(_ req: Request) -> Tag } ================================================ FILE: Chapter 16/myProject/Sources/App/configure.swift ================================================ import Vapor import Fluent import FluentSQLiteDriver import Liquid import LiquidLocalDriver @_exported import AppApi public func configure( _ app: Application ) throws { app.fileStorages.use( .local( publicUrl: "http://localhost:8080", publicPath: app.directory.publicDirectory, workDirectory: "assets" ), as: .local ) app.routes.defaultMaxBodySize = "10mb" let dbPath = app.directory.resourcesDirectory + "db.sqlite" app.databases.use(.sqlite(.file(dbPath)), as: .sqlite) app.middleware.use( FileMiddleware( publicDirectory: app.directory.publicDirectory ) ) app.middleware.use(ExtendPathMiddleware()) app.sessions.use(.fluent) app.migrations.add(SessionRecord.migration) app.middleware.use(app.sessions.middleware) let modules: [ModuleInterface] = [ WebModule(), UserModule(), AdminModule(), ApiModule(), BlogModule(), ] for module in modules { try module.boot(app) } for module in modules { try module.setUp(app) } try app.autoMigrate().wait() } ================================================ FILE: Chapter 16/myProject/Sources/App/routes.swift ================================================ import Vapor import SwiftHtml func routes(_ app: Application) throws { app.routes.get { req -> Response in req.templates.renderHtml( WebIndexTemplate( WebIndexContext( title: "Home" ) ) { P("Hi there, welcome to my page!") } ) } } ================================================ FILE: Chapter 16/myProject/Sources/AppApi/Framework/ApiModelInterface.swift ================================================ public protocol ApiModelInterface { associatedtype Module: ApiModuleInterface static var pathKey: String { get } static var pathIdKey: String { get } } public extension ApiModelInterface { static var pathKey: String { String(describing: self).lowercased() + "s" } static var pathIdKey: String { String(describing: self).lowercased() + "Id" } } ================================================ FILE: Chapter 16/myProject/Sources/AppApi/Framework/ApiModuleInterface.swift ================================================ public protocol ApiModuleInterface { static var pathKey: String { get } } public extension ApiModuleInterface { static var pathKey: String { String(describing: self).lowercased() } } ================================================ FILE: Chapter 16/myProject/Sources/AppApi/Modules/Blog/Blog.swift ================================================ public enum Blog: ApiModuleInterface { public enum Post: ApiModelInterface { public typealias Module = Blog } public enum Category: ApiModelInterface { public typealias Module = Blog public static let pathKey: String = "categories" } } ================================================ FILE: Chapter 16/myProject/Sources/AppApi/Modules/Blog/BlogCategory.swift ================================================ import Foundation public extension Blog.Category { struct List: Codable { public let id: UUID public let title: String public init( id: UUID, title: String ) { self.id = id self.title = title } } struct Detail: Codable { public let id: UUID public let title: String public init( id: UUID, title: String ) { self.id = id self.title = title } } struct Create: Codable { public let title: String public init( title: String ) { self.title = title } } struct Update: Codable { public let title: String public init( title: String ) { self.title = title } } struct Patch: Codable { public let title: String? public init( title: String? ) { self.title = title } } } ================================================ FILE: Chapter 16/myProject/Sources/AppApi/Modules/Blog/BlogPost.swift ================================================ import Foundation public extension Blog.Post { struct List: Codable { public let id: UUID public let title: String public let slug: String public let image: String public let excerpt: String public let date: Date public init( id: UUID, title: String, slug: String, image: String, excerpt: String, date: Date ) { self.id = id self.title = title self.slug = slug self.image = image self.excerpt = excerpt self.date = date } } struct Detail: Codable { public let id: UUID public let title: String public let slug: String public let image: String public let excerpt: String public let date: Date public let category: Blog.Category.List public let content: String public init( id: UUID, title: String, slug: String, image: String, excerpt: String, date: Date, category: Blog.Category.List, content: String ) { self.id = id self.title = title self.slug = slug self.image = image self.excerpt = excerpt self.date = date self.category = category self.content = content } } struct Create: Codable { public let title: String public let slug: String public let image: String public let excerpt: String public let date: Date public let content: String public let categoryId: UUID public init( title: String, slug: String, image: String, excerpt: String, date: Date, content: String, categoryId: UUID ) { self.title = title self.slug = slug self.image = image self.excerpt = excerpt self.date = date self.content = content self.categoryId = categoryId } } struct Update: Codable { public let title: String public let slug: String public let image: String public let excerpt: String public let date: Date public let content: String public let categoryId: UUID public init( title: String, slug: String, image: String, excerpt: String, date: Date, content: String, categoryId: UUID ) { self.title = title self.slug = slug self.image = image self.excerpt = excerpt self.date = date self.content = content self.categoryId = categoryId } } struct Patch: Codable { public let title: String? public let slug: String? public let image: String? public let excerpt: String? public let date: Date? public let content: String? public let categoryId: UUID? public init( title: String?, slug: String?, image: String?, excerpt: String?, date: Date?, content: String?, categoryId: UUID? ) { self.title = title self.slug = slug self.image = image self.excerpt = excerpt self.date = date self.content = content self.categoryId = categoryId } } } ================================================ FILE: Chapter 16/myProject/Sources/AppApi/Modules/User/User.swift ================================================ public enum User: ApiModuleInterface { public enum Account: ApiModelInterface { public typealias Module = User } public enum Token: ApiModelInterface { public typealias Module = User } } ================================================ FILE: Chapter 16/myProject/Sources/AppApi/Modules/User/UserAccount.swift ================================================ import Foundation public extension User.Account { struct Login: Codable { public let email: String public let password: String public init( email: String, password: String ) { self.email = email self.password = password } } struct List: Codable { public let id: UUID public let email: String public init( id: UUID, email: String ) { self.id = id self.email = email } } struct Detail: Codable { public let id: UUID public let email: String public init( id: UUID, email: String ) { self.id = id self.email = email } } struct Create: Codable { public let email: String public let password: String public init( email: String, password: String ) { self.email = email self.password = password } } struct Update: Codable { public let email: String public let password: String? public init( email: String, password: String ) { self.email = email self.password = password } } struct Patch: Codable { public let email: String? public let password: String? public init( email: String?, password: String? ) { self.email = email self.password = password } } } ================================================ FILE: Chapter 16/myProject/Sources/AppApi/Modules/User/UserToken.swift ================================================ import Foundation public extension User.Token { struct Detail: Codable { public let id: UUID public let value: String public let user: User.Account.Detail public init( id: UUID, value: String, user: User.Account.Detail ) { self.id = id self.value = value self.user = user } } } ================================================ FILE: Chapter 16/myProject/Sources/Run/main.swift ================================================ import App import Vapor var env = try Environment.detect() try LoggingSystem.bootstrap(from: &env) let app = Application(env) defer { app.shutdown() } try configure(app) try app.run() ================================================ FILE: Chapter 16/myProject/Tests/AppApiTests/AppApiTests.swift ================================================ @testable import AppApi import XCTest final class AppApiTests: XCTestCase { enum HTTPError: Error { case invalidResponse case invalidStatusCode(Int) } let baseUrl = URL(string: "http://localhost:8080/api/")! private func authenticate( _ login: User.Account.Login ) async throws -> User.Token.Detail { var req = URLRequest(url: baseUrl.appendingPathComponent("sign-in/")) req.httpMethod = "POST" req.addValue("application/json", forHTTPHeaderField: "Content-Type") req.httpBody = try JSONEncoder().encode(login) let (data, response) = try await URLSession.shared.data(for: req) guard let response = response as? HTTPURLResponse else { throw HTTPError.invalidResponse } guard 200...299 ~= response.statusCode else { throw HTTPError.invalidStatusCode(response.statusCode) } return try JSONDecoder().decode(User.Token.Detail.self, from: data) } private func authenticateRoot() async throws -> User.Token.Detail { try await authenticate( .init( email: "root@localhost.com", password: "ChangeMe1" ) ) } func testAuthorization() async throws { let login = User.Account.Login( email: "root@localhost.com", password: "ChangeMe1" ) let token = try await authenticate(login) XCTAssertEqual(token.value.count, 64) XCTAssertEqual(token.user.email, login.email) } func testBlogCategories() async throws { let token = try await authenticateRoot() let path = "\(Blog.pathKey)/\(Blog.Category.pathKey)/" var req = URLRequest(url: baseUrl.appendingPathComponent(path)) req.addValue("Bearer \(token.value)", forHTTPHeaderField: "Authorization") let (data, response) = try await URLSession.shared.data(for: req) guard let response = response as? HTTPURLResponse else { throw HTTPError.invalidResponse } guard 200...299 ~= response.statusCode else { throw HTTPError.invalidStatusCode(response.statusCode) } let decoder = JSONDecoder() let categories = try decoder.decode([Blog.Category.List].self, from: data) XCTAssertFalse(categories.isEmpty) } } ================================================ FILE: Chapter 16/myProject/Tests/AppTests/AppTests.swift ================================================ @testable import App import XCTVapor final class AppTests: AppTestCase { func testHomePage() throws { let app = try createTestApp() defer { app.shutdown() } try app.testable(method: .inMemory).test(.GET, "") { res in XCTAssertEqual(res.status, .ok) let contentType = try XCTUnwrap(res.headers.contentType) XCTAssertEqual(contentType, .html) XCTAssertTrue(res.body.string.contains("Home")) } } func testAuth() throws { let app = try createTestApp() defer { app.shutdown() } let email = "root@localhost.com" let token = try authenticate( .init( email: email, password: "ChangeMe1" ), app ) XCTAssertEqual(token.user.email, email) } } ================================================ FILE: Chapter 16/myProject/Tests/AppTests/BlogCategoryApiTests.swift ================================================ @testable import App import XCTVapor extension Blog.Category.Create: Content {} final class BlogCategoryApiTests: AppTestCase { func testList() throws { let app = try createTestApp() let token = try authenticateRoot(app) defer { app.shutdown() } let headers = HTTPHeaders([("Authorization", "Bearer \(token.value)")]) try app //.testable(method: .inMemory) .testable(method: .running(port: 8081)) .test(.GET, "/api/blog/categories/", headers: headers) { res in XCTAssertEqual(res.status, .ok) let contentType = try XCTUnwrap(res.headers.contentType) XCTAssertEqual(contentType, .json) XCTAssertContent([Blog.Category.List].self, res) { content in XCTAssertEqual(content.count, 4) } } } func testCreate() throws { let app = try createTestApp() let token = try authenticateRoot(app) defer { app.shutdown() } let headers = HTTPHeaders([("Authorization", "Bearer \(token.value)")]) let newCategory = Blog.Category.Create(title: "Test category") try app.test( .POST, "/api/blog/categories/", headers: headers, content: newCategory ) { res in XCTAssertEqual(res.status, .created) let contentType = try XCTUnwrap(res.headers.contentType) XCTAssertEqual(contentType, .json) XCTAssertContent(Blog.Category.Detail.self, res) { content in XCTAssertEqual(content.title, newCategory.title) } } } func testCreateListUpdate() throws { let app = try createTestApp() let token = try authenticateRoot(app) defer { app.shutdown() } let headers = HTTPHeaders([("Authorization", "Bearer \(token.value)")]) let newCategory = Blog.Category.Create(title: "Test category") try app .test( .POST, "/api/blog/categories/", headers: headers, content: newCategory ) { res in XCTAssertEqual(res.status, .created) let contentType = try XCTUnwrap(res.headers.contentType) XCTAssertEqual(contentType, .json) XCTAssertContent(Blog.Category.Detail.self, res) { content in XCTAssertEqual(content.title, newCategory.title) } } .test( .GET, "/api/blog/categories/", headers: headers ) { res in XCTAssertEqual(res.status, .ok) let contentType = try XCTUnwrap(res.headers.contentType) XCTAssertEqual(contentType, .json) XCTAssertContent([Blog.Category.List].self, res) { content in XCTAssertEqual(content.count, 5) } } } } ================================================ FILE: Chapter 16/myProject/Tests/AppTests/BlogPostApiTests.swift ================================================ @testable import App import XCTVapor import Spec extension Blog.Post.Create: Content {} extension Blog.Post.Update: Content {} final class BlogPostApiTests: AppTestCase { func testList() throws { let app = try createTestApp() let token = try authenticateRoot(app) defer { app.shutdown() } try app .describe("Blog posts list API should be fine") .get("/api/blog/posts/") .bearerToken(token.value) .expect(.ok) .expect(.json) .expect([Blog.Post.List].self) { content in XCTAssertEqual(content.count, 9) } .test() } func testCreate() async throws { let app = try createTestApp() let token = try authenticateRoot(app) defer { app.shutdown() } let category = try await BlogCategoryModel.query(on: app.db).first() guard let category = category else { XCTFail("Missing default category") throw Abort(.notFound) } let newPost = Blog.Post.Create( title: "Dummy post", slug: "dummy-slug", image: "/dummy/image.jpg", excerpt: "Lorem ipsum", date: Date(), content: "Lorem ipsum", categoryId: category.id! ) try app .describe("Create post should be fine") .post("/api/blog/posts/") .body(newPost) .bearerToken(token.value) .expect(.created) .expect(.json) .expect(Blog.Post.Detail.self) { content in XCTAssertEqual(content.title, newPost.title) } .test() } func testUpdate() async throws { let app = try self.createTestApp() let token = try authenticateRoot(app) defer { app.shutdown() } guard let post = try await BlogPostModel .query(on: app.db) .with(\.$category) .first() else { XCTFail("Missing blog post") throw Abort(.notFound) } let suffix = " updated" let newPost = Blog.Post.Update( title: post.title + suffix, slug: post.slug + suffix, image: post.imageKey + suffix, excerpt: post.excerpt + suffix, date: post.date, content: post.content + suffix, categoryId: post.category.id! ) try app .describe("Update post should be fine") .put("/api/blog/posts/\(post.id!.uuidString)/") .body(newPost) .bearerToken(token.value) .expect(.ok) .expect(.json) .expect(Blog.Post.Detail.self) { content in XCTAssertEqual(content.id, post.id) XCTAssertEqual(content.title, newPost.title) XCTAssertEqual(content.slug, newPost.slug) XCTAssertEqual(content.image, newPost.image) XCTAssertEqual(content.excerpt, newPost.excerpt) XCTAssertEqual(content.content, newPost.content) } .test() } } ================================================ FILE: Chapter 16/myProject/Tests/AppTests/Framework/AppTestCase.swift ================================================ @testable import App import XCTVapor class AppTestCase: XCTestCase { struct UserLogin: Content { let email: String let password: String } func createTestApp() throws -> Application { let app = Application(.testing) try configure(app) app.databases.reinitialize() app.databases.use(.sqlite(.memory), as: .sqlite) app.databases.default(to: .sqlite) try app.autoMigrate().wait() return app } func authenticate( _ user: UserLogin, _ app: Application ) throws -> User.Token.Detail { var token: User.Token.Detail? try app.test(.POST, "/api/sign-in/", beforeRequest: { req in try req.content.encode(user) }, afterResponse: { res in XCTAssertContent(User.Token.Detail.self, res) { content in token = content } }) guard let result = token else { XCTFail("Login failed") throw Abort(.unauthorized) } return result } func authenticateRoot( _ app: Application ) throws -> User.Token.Detail { try authenticate( .init( email: "root@localhost.com", password: "ChangeMe1" ), app ) } } ================================================ FILE: Chapter 16/myProject/Tests/AppTests/Framework/XCTApplicationTester.swift ================================================ import XCTVapor extension XCTApplicationTester { @discardableResult public func test( _ method: HTTPMethod, _ path: String, headers: HTTPHeaders = [:], content: T, afterResponse: (XCTHTTPResponse) throws -> () = { _ in } ) throws -> XCTApplicationTester where T: Content { try test(method, path, headers: headers, beforeRequest: { req in try req.content.encode(content) }, afterResponse: afterResponse) } } ================================================ FILE: Chapter 16/myProject/docker-compose.yml ================================================ # 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 # Stop all: docker compose down # x-shared_environment: &shared_environment LOG_LEVEL: ${LOG_LEVEL:-debug} services: app: image: myproject:latest build: context: . environment: <<: *shared_environment 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"] ================================================ FILE: README.md ================================================ # 📖 Practical Server Side Swift Vapor 4 code samples for the [Practical Server Side Swift book](https://gumroad.com/l/practical-server-side-swift) available on [Gumroad](https://gumroad.com/l/practical-server-side-swift). **Chapter 1: Introduction** We start with an introduction to the Server Side Swift world, explaining the evolution of Swift as a universal programming language. We'll talk about both the strengths and weaknesses of the language and discuss why Swift is a good choice to build backend applications. We'll explore the Swift ecosystem and the open-source movement that made it possible to create the necessary tools on Linux to turn Swift into a server-side language. You'll get introduced to Vapor, the most popular web application framework, that we're going to use in this book. **Chapter 2: Getting started with Vapor** Next, we go over detailed instructions about how to install all the required components to build server-side Swift applications both on Linux and macOS. You'll meet some command-line tools that can help your everyday life as a backend developer and we'll create our very first Vapor project using the Swift Package Manager. We'll also set up the Vapor toolbox, a handy little tool that can help you bootstrap projects based on a template. In the very last section, we'll briefly take a look at the architecture of a Vapor application. **Chapter 3: Getting started with SwiftHtml** Then we're going to build our first website using the SwiftHtml library, and we're going to generate HTML code through Swift by creating template files using a Domain Specific Language (DSL). You'll learn about how to connect SwiftHtml with Vapor and how to render HTML by using context variables to provide additional template data. You'll learn about the syntax of SwiftHtml, how to iterate through objects, how to check optional variables, and how to extend a base template to provide a reusable framework for our website, and finally, we'll build a simple blog layout with a post, list, and detail pages. **Chapter 4: Getting started with Fluent** Here you'll learn about the Fluent ORM framework and the advantages of using such a tool instead of writing raw database queries. We'll set up Fluent, powered by the SQLite driver, and model our database fields using property wrappers in Swift. We're going to provide a seed for our database, get familiar with migration scripts, and make some changes on the website so it can query blog posts from the local database and render them using view templates. **Chapter 5: Sessions and user authentication** We're going to focus on building a session-based web authentication layer that users will be able to use to sign in using a form, and with which already logged in users will be authenticated with the help of a session cookie and persistent session storage using Fluent. In the second half of this chapter, I'll show you how to create a custom authenticator middleware that'll allow you to authenticate users based on sessions or credentials. **Chapter 6: Abstract forms and form fields** Building forms is all about creating an abstract form builder that we can use to generate the HTML forms. We're going to define reusable form fields with corresponding context objects using a model-view-like architecture. This will allow us to compose all kinds of input forms by reusing the generic fields. In the second half of the chapter, we're going to talk about processing user input and loading and persisting data using a protocol-oriented solution. Finally, we're going to rebuild our already existing user login form by using those components. **Chapter 7: Form events and async validation** Next, we're going to work a little bit on our form components. We're going to implement more event handler methods and you're going to learn the preferred way of calling them to build a proper create or update workflow flow. The second half of the chapter is all about building an asynchronous validation mechanism for the abstract forms. We're going to build several form field validators and finally, you'll see how to work with these validators and display user errors to improve the overall experience. **Chapter 8: Advanced form fields** This chapter is all about building new form fields that we're going to use later on. You'll learn how to build custom form fields based on the abstract form field class, so by the end of this chapter, you should be able to create even more form fields to fit your needs. We're also going to introduce a brand new Swift package called Liquid that's a file storage driver made for Vapor. By using this library, we're going to be able to create a form field for uploading images. **Chapter 9: Content Management System** Here you'll learn how to build a basic content management system with an admin interface. We're going to create a standalone module for the admin views that'll be completely separated from the web frontend. The CMS will support list, detail, create, update and delete functionality. Models are going to be persisted to the database and we'll secure the admin endpoints by using a new built-in middleware. **Chapter 10: Building a generic admin interface** This chapter is about turning our basic CMS into a generic solution. By leveraging the power of Swift protocols, we're going to be able to come up with several base controllers that can be used to manage database models through the admin interface. This methodology allows us to easily define a list, create, update and delete controllers. By the end of this chapter, we're going to have a completely working admin solution for the blog module. **Chapter 11: A basic REST API layer** Next, you'll learn about building a standard JSON-based API service. In the first section, we'll discuss how to design a REST API then we'll build the CRUD endpoints for the category controller. We'll also talk a bit about the HTTP layer and learn how to use the cURL command-line utility to test the endpoints. You'll discover why it's a better practice to use standalone data transfer objects (DTOs) rather than expose database models to the public. **Chapter 12: Building a generic REST API** This chapter contains useful materials about how to turn our REST API layer into a reusable generic solution. We're going to define common protocols that'll allow us to share some of the logic between the admin and API controllers. The first part's going to be all about the controller updates, but later on in this chapter, we're also going to improve the routing mechanism by introducing new setup methods for the route handlers. **Chapter 13: API protection and validation** Here you'll learn about making the backend service more secure by introducing better API protection and validation methods. The first part is about user authentication using bearer tokens. We're going to create a new token-based authenticator and guard the API endpoints against unauthenticated requests. The second part is going to be all about data validation using the async validator logic that we created a few chapters before. In the very last section of this chapter, we're going to introduce some additional lifecycle methods for the controllers. **Chapter 14: System under testing** For testing, you'll learn the brand new **XCTVapor** framework. First, we'll set up the test environment, write some basic unit tests for our application, and then run them. Next, we're going to dig a little bit deeper into the **XCTVapor** framework so you can see how to write more complex tests. In the last part, you'll meet a super lightweight and clean testing tool. The **Spec** library will allow us to write declarative specifications for our test cases. **Chapter 15: Event-driven hook functions** After that, we're going to eliminate the dependencies between the modules by introducing a brand new event-driven architecture (EDA). By using hook functions, we're going to be able to build connections without the need of importing the interface of one module into another. The EDA design pattern allows us to create loosely coupled software components and services without forming an actual dependency between the participants. **Chapter 16: Shared API library packages** Last but not least, this chapter teaches you how to separate the data transfer object (DTO) layer into a standalone Swift package product: this way you'll be able to share server-side Swift code with client apps. In the first part of the chapter, I'm going to show you how to set up the project then we're going to add access control modifiers to allow other modules to see our DTOs. The second half of the chapter is going to give you some really basic examples of how to perform HTTP requests using the modern Swift concurrency APIs. ## Do you have any questions? Feedbacks, release schedule, beta period anything you would like to know? Feel free to send me your thoughts so I can improve both the samples and the book. Please don't hesitate to contact me using the options below. ## Contact details - Web: [theswiftdev.com](https://theswiftdev.com) - Email: [mail.tib@gmail.com](mailto:mail.tib@gmail.com) - Twitter: [@tiborbodecs](https://twitter.com/tiborbodecs) I hope you'll enjoy reading my book. Thank you for your support.