Showing preview only (1,765K chars total). Download the full file or copy to clipboard to get everything.
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..<categories.count)].id!
)
}
.create(on: db)
}
func revert(on db: Database) async throws {
try await BlogPostModel.query(on: db).delete()
try await BlogCategoryModel.query(on: db).delete()
}
}
}
================================================
FILE: Chapter 04/myProject/Sources/App/Modules/Blog/Database/Models/BlogCategoryModel.swift
================================================
import Vapor
import Fluent
final class BlogCategoryModel: DatabaseModelInterface {
typealias Module = BlogModule
static let identifier = "categories"
struct FieldKeys {
struct v1 {
static var title: FieldKey { "title" }
}
}
@ID() var id: UUID?
@Field(key: FieldKeys.v1.title)
var title: String
@Children(for: \.$category)
var posts: [BlogPostModel]
init() { }
init(
id: UUID? = nil,
title: String
) {
self.id = id
self.title = title
}
}
================================================
FILE: Chapter 04/myProject/Sources/App/Modules/Blog/Database/Models/BlogPostModel.swift
================================================
import Vapor
import Fluent
final class BlogPostModel: DatabaseModelInterface {
typealias Module = BlogModule
struct FieldKeys {
struct v1 {
static var title: FieldKey { "title" }
static var slug: FieldKey { "slug" }
static var imageKey: FieldKey { "image_key" }
static var excerpt: FieldKey { "excerpt" }
static var date: FieldKey { "date" }
static var content: FieldKey { "content" }
static var categoryId: FieldKey { "category_id" }
}
}
@ID()
var id: UUID?
@Field(key: FieldKeys.v1.title)
var title: String
@Field(key: FieldKeys.v1.slug)
var slug: String
@Field(key: FieldKeys.v1.imageKey)
var imageKey: String
@Field(key: FieldKeys.v1.excerpt)
var excerpt: String
@Field(key: FieldKeys.v1.date)
var date: Date
@Field(key: FieldKeys.v1.content)
var content: String
@Parent(key: FieldKeys.v1.categoryId)
var category: BlogCategoryModel
init() { }
init(
id: UUID? = nil,
title: String,
slug: String,
imageKey: String,
excerpt: String,
date: Date,
content: String,
categoryId: UUID)
{
self.id = id
self.title = title
self.slug = slug
self.imageKey = imageKey
self.excerpt = excerpt
self.date = date
self.content = content
$category.id = categoryId
}
}
================================================
FILE: Chapter 04/myProject/Sources/App/Modules/Blog/Objects/Blog.swift
================================================
enum Blog {
enum Post {
}
enum Category {
}
}
================================================
FILE: Chapter 04/myProject/Sources/App/Modules/Blog/Objects/BlogCategory.swift
================================================
import Foundation
extension Blog.Category {
struct List: Codable {
let id: UUID
let title: String
}
}
================================================
FILE: Chapter 04/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 04/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostContext.swift
================================================
struct BlogPostContext {
let post: Blog.Post.Detail
}
================================================
FILE: Chapter 04/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 04/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 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..<categories.count)].id!
)
}
.create(on: db)
}
func revert(on db: Database) async throws {
try await BlogPostModel.query(on: db).delete()
try await BlogCategoryModel.query(on: db).delete()
}
}
}
================================================
FILE: Chapter 05/myProject/Sources/App/Modules/Blog/Database/Models/BlogCategoryModel.swift
================================================
import Vapor
import Fluent
final class BlogCategoryModel: DatabaseModelInterface {
typealias Module = BlogModule
static let identifier = "categories"
struct FieldKeys {
struct v1 {
static var title: FieldKey { "title" }
}
}
@ID() var id: UUID?
@Field(key: FieldKeys.v1.title)
var title: String
@Children(for: \.$category)
var posts: [BlogPostModel]
init() { }
init(
id: UUID? = nil,
title: String
) {
self.id = id
self.title = title
}
}
================================================
FILE: Chapter 05/myProject/Sources/App/Modules/Blog/Database/Models/BlogPostModel.swift
================================================
import Vapor
import Fluent
final class BlogPostModel: DatabaseModelInterface {
typealias Module = BlogModule
struct FieldKeys {
struct v1 {
static var title: FieldKey { "title" }
static var slug: FieldKey { "slug" }
static var imageKey: FieldKey { "image_key" }
static var excerpt: FieldKey { "excerpt" }
static var date: FieldKey { "date" }
static var content: FieldKey { "content" }
static var categoryId: FieldKey { "category_id" }
}
}
@ID()
var id: UUID?
@Field(key: FieldKeys.v1.title)
var title: String
@Field(key: FieldKeys.v1.slug)
var slug: String
@Field(key: FieldKeys.v1.imageKey)
var imageKey: String
@Field(key: FieldKeys.v1.excerpt)
var excerpt: String
@Field(key: FieldKeys.v1.date)
var date: Date
@Field(key: FieldKeys.v1.content)
var content: String
@Parent(key: FieldKeys.v1.categoryId)
var category: BlogCategoryModel
init() { }
init(
id: UUID? = nil,
title: String,
slug: String,
imageKey: String,
excerpt: String,
date: Date,
content: String,
categoryId: UUID)
{
self.id = id
self.title = title
self.slug = slug
self.imageKey = imageKey
self.excerpt = excerpt
self.date = date
self.content = content
$category.id = categoryId
}
}
================================================
FILE: Chapter 05/myProject/Sources/App/Modules/Blog/Objects/Blog.swift
================================================
enum Blog {
enum Post {
}
enum Category {
}
}
================================================
FILE: Chapter 05/myProject/Sources/App/Modules/Blog/Objects/BlogCategory.swift
================================================
import Foundation
extension Blog.Category {
struct List: Codable {
let id: UUID
let title: String
}
}
================================================
FILE: Chapter 05/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 05/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostContext.swift
================================================
struct BlogPostContext {
let post: Blog.Post.Detail
}
================================================
FILE: Chapter 05/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 05/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 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<Input, Output>) -> 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..<categories.count)].id!
)
}
.create(on: db)
}
func revert(on db: Database) async throws {
try await BlogPostModel.query(on: db).delete()
try await BlogCategoryModel.query(on: db).delete()
}
}
}
================================================
FILE: Chapter 06/myProject/Sources/App/Modules/Blog/Database/Models/BlogCategoryModel.swift
================================================
import Vapor
import Fluent
final class BlogCategoryModel: DatabaseModelInterface {
typealias Module = BlogModule
static let identifier = "categories"
struct FieldKeys {
struct v1 {
static var title: FieldKey { "title" }
}
}
@ID() var id: UUID?
@Field(key: FieldKeys.v1.title)
var title: String
@Children(for: \.$category)
var posts: [BlogPostModel]
init() { }
init(
id: UUID? = nil,
title: String
) {
self.id = id
self.title = title
}
}
================================================
FILE: Chapter 06/myProject/Sources/App/Modules/Blog/Database/Models/BlogPostModel.swift
================================================
import Vapor
import Fluent
final class BlogPostModel: DatabaseModelInterface {
typealias Module = BlogModule
struct FieldKeys {
struct v1 {
static var title: FieldKey { "title" }
static var slug: FieldKey { "slug" }
static var imageKey: FieldKey { "image_key" }
static var excerpt: FieldKey { "excerpt" }
static var date: FieldKey { "date" }
static var content: FieldKey { "content" }
static var categoryId: FieldKey { "category_id" }
}
}
@ID()
var id: UUID?
@Field(key: FieldKeys.v1.title)
var title: String
@Field(key: FieldKeys.v1.slug)
var slug: String
@Field(key: FieldKeys.v1.imageKey)
var imageKey: String
@Field(key: FieldKeys.v1.excerpt)
var excerpt: String
@Field(key: FieldKeys.v1.date)
var date: Date
@Field(key: FieldKeys.v1.content)
var content: String
@Parent(key: FieldKeys.v1.categoryId)
var category: BlogCategoryModel
init() { }
init(
id: UUID? = nil,
title: String,
slug: String,
imageKey: String,
excerpt: String,
date: Date,
content: String,
categoryId: UUID)
{
self.id = id
self.title = title
self.slug = slug
self.imageKey = imageKey
self.excerpt = excerpt
self.date = date
self.content = content
$category.id = categoryId
}
}
================================================
FILE: Chapter 06/myProject/Sources/App/Modules/Blog/Objects/Blog.swift
================================================
enum Blog {
enum Post {
}
enum Category {
}
}
================================================
FILE: Chapter 06/myProject/Sources/App/Modules/Blog/Objects/BlogCategory.swift
================================================
import Foundation
extension Blog.Category {
struct List: Codable {
let id: UUID
let title: String
}
}
================================================
FILE: Chapter 06/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 06/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostContext.swift
================================================
struct BlogPostContext {
let post: Blog.Post.Detail
}
================================================
FILE: Chapter 06/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 06/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 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<Input, Output>) async throws -> Void
public typealias FormFieldValidatorsBlock =
((Request, AbstractFormField<Input, Output>) -> [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<Input, Output>) -> 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<Input, Output>,
_ message: String? = nil
) -> FormFieldValidator<Input, Output> {
let msg = message ??
"\(field.key.capitalized) is required"
return .init(field, msg) { _, field in
!field.input.isEmpty
}
}
static func min(
_ field: AbstractFormField<Input, Output>,
length: Int,
message: String? = nil
) -> FormFieldValidator<Input, Output> {
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<Input, Output>,
length: Int,
message: String? = nil
) -> FormFieldValidator<Input, Output> {
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<Input, Output>,
message: String? = nil
) -> FormFieldValidator<Input, Output> {
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<Input, Output>,
message: String? = nil
) -> FormFieldValidator<Input, Output> {
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<Input, Output>) async throws -> Bool)
public let field: AbstractFormField<Input, Output>
public let message: String
public let validation: AsyncValidationBlock
public var key: String { field.key }
public init(
_ field: AbstractFormField<Input, Output>,
_ 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..<categories.count)].id!
)
}
.create(on: db)
}
func revert(on db: Database) async throws {
try await BlogPostModel.query(on: db).delete()
try await BlogCategoryModel.query(on: db).delete()
}
}
}
================================================
FILE: Chapter 07/myProject/Sources/App/Modules/Blog/Database/Models/BlogCategoryModel.swift
================================================
import Vapor
import Fluent
final class BlogCategoryModel: DatabaseModelInterface {
typealias Module = BlogModule
static let identifier = "categories"
struct FieldKeys {
struct v1 {
static var title: FieldKey { "title" }
}
}
@ID() var id: UUID?
@Field(key: FieldKeys.v1.title)
var title: String
@Children(for: \.$category)
var posts: [BlogPostModel]
init() { }
init(
id: UUID? = nil,
title: String
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
SYMBOL INDEX (14 symbols across 14 files)
FILE: Chapter 03/myProject/Public/js/web.js
function about (line 1) | function about() {
FILE: Chapter 04/myProject/Public/js/web.js
function about (line 1) | function about() {
FILE: Chapter 05/myProject/Public/js/web.js
function about (line 1) | function about() {
FILE: Chapter 06/myProject/Public/js/web.js
function about (line 1) | function about() {
FILE: Chapter 07/myProject/Public/js/web.js
function about (line 1) | function about() {
FILE: Chapter 08/myProject/Public/js/web.js
function about (line 1) | function about() {
FILE: Chapter 09/myProject/Public/js/web.js
function about (line 1) | function about() {
FILE: Chapter 10/myProject/Public/js/web.js
function about (line 1) | function about() {
FILE: Chapter 11/myProject/Public/js/web.js
function about (line 1) | function about() {
FILE: Chapter 12/myProject/Public/js/web.js
function about (line 1) | function about() {
FILE: Chapter 13/myProject/Public/js/web.js
function about (line 1) | function about() {
FILE: Chapter 14/myProject/Public/js/web.js
function about (line 1) | function about() {
FILE: Chapter 15/myProject/Public/js/web.js
function about (line 1) | function about() {
FILE: Chapter 16/myProject/Public/js/web.js
function about (line 1) | function about() {
Condensed preview — 1555 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,567K chars).
[
{
"path": ".gitignore",
"chars": 106,
"preview": ".DS_Store\n.build\n.swiftpm\nxcuserdata\nMyProject.xcodeproj\nPackages\nDerivedData/\ndb.sqlite\nPackage.resolved\n"
},
{
"path": "Changelog.md",
"chars": 1988,
"preview": "## Changelog - Practical Server Side Swift\n\nDon't forget to check the sample code repository on [GitHub](https://github."
},
{
"path": "Chapter 01/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "Chapter 02/SPM/.gitignore",
"chars": 53,
"preview": ".DS_Store\n/.build\n/Packages\n/*.xcodeproj\nxcuserdata/\n"
},
{
"path": "Chapter 02/SPM/Package.swift",
"chars": 517,
"preview": "// swift-tools-version:6.1\nimport PackageDescription\n\nlet package = Package(\n name: \"myProject\",\n platforms: [\n "
},
{
"path": "Chapter 02/SPM/README.md",
"chars": 44,
"preview": "# myProject\n\nA description of this package.\n"
},
{
"path": "Chapter 02/SPM/Sources/main.swift",
"chars": 256,
"preview": "import Vapor\n\nlet env = try Environment.detect()\nlet app = try await Application.make(env)\n\ndo {\n app.get { req in \"H"
},
{
"path": "Chapter 02/VaporToolbox/.dockerignore",
"chars": 18,
"preview": ".build/\n.swiftpm/\n"
},
{
"path": "Chapter 02/VaporToolbox/.gitignore",
"chars": 106,
"preview": "Packages\n.build\nxcuserdata\n*.xcodeproj\nDerivedData/\n.DS_Store\ndb.sqlite\n.swiftpm\n.env\n.env.*\n!.env.example"
},
{
"path": "Chapter 02/VaporToolbox/Dockerfile",
"chars": 3336,
"preview": "# ================================\n# Build image\n# ================================\nFROM swift:6.0-noble AS build\n\n# Ins"
},
{
"path": "Chapter 02/VaporToolbox/Package.swift",
"chars": 1039,
"preview": "// swift-tools-version:6.0\nimport PackageDescription\n\nlet package = Package(\n name: \"myProject\",\n platforms: [\n "
},
{
"path": "Chapter 02/VaporToolbox/Public/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "Chapter 02/VaporToolbox/README.md",
"chars": 595,
"preview": "# myProject\n\n💧 A project built with the Vapor web framework.\n\n## Getting Started\n\nTo build the project using the Swift P"
},
{
"path": "Chapter 02/VaporToolbox/Sources/myProject/Controllers/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "Chapter 02/VaporToolbox/Sources/myProject/configure.swift",
"chars": 290,
"preview": "import Vapor\n\n// configures your application\npublic func configure(_ app: Application) async throws {\n // uncomment t"
},
{
"path": "Chapter 02/VaporToolbox/Sources/myProject/entrypoint.swift",
"chars": 553,
"preview": "import Vapor\nimport Logging\nimport NIOCore\nimport NIOPosix\n\n@main\nenum Entrypoint {\n\n static func main() async throws"
},
{
"path": "Chapter 02/VaporToolbox/Sources/myProject/routes.swift",
"chars": 187,
"preview": "import Vapor\n\nfunc routes(_ app: Application) throws {\n app.get { req async in\n \"It works!\"\n }\n\n app.get"
},
{
"path": "Chapter 02/VaporToolbox/Tests/myProjectTests/myProjectTests.swift",
"chars": 460,
"preview": "@testable import myProject\nimport VaporTesting\nimport Testing\n\n@Suite(\"App Tests\")\nstruct myProjectTests {\n\n @Test(\"T"
},
{
"path": "Chapter 02/VaporToolbox/docker-compose.yml",
"chars": 854,
"preview": "# Docker Compose file for Vapor\n#\n# Install Docker on your system to run and test\n# your Vapor app in a production-like "
},
{
"path": "Chapter 03/myProject/Dockerfile",
"chars": 3336,
"preview": "# ================================\n# Build image\n# ================================\nFROM swift:6.0-noble AS build\n\n# Ins"
},
{
"path": "Chapter 03/myProject/Package.swift",
"chars": 1049,
"preview": "// swift-tools-version:6.0\nimport PackageDescription\n\nlet package = Package(\n name: \"myProject\",\n platforms: [\n "
},
{
"path": "Chapter 03/myProject/Public/css/web.css",
"chars": 35,
"preview": "#blog h2 {\n margin: 0.5rem 0;\n}\n"
},
{
"path": "Chapter 03/myProject/Public/js/web.js",
"chars": 59,
"preview": "function about() {\n alert(\"myPage\\n\\nversion 1.0.0\");\n}\n"
},
{
"path": "Chapter 03/myProject/Sources/myProject/Middlewares/ExtendPathMiddleware.swift",
"chars": 470,
"preview": "import Vapor\n\nstruct ExtendPathMiddleware: AsyncMiddleware {\n\n func respond(\n to req: Request,\n chainin"
},
{
"path": "Chapter 03/myProject/Sources/myProject/Modules/Blog/BlogPost.swift",
"chars": 206,
"preview": "import Foundation\n\nstruct BlogPost: Codable {\n let title: String\n let slug: String\n let image: String\n let e"
},
{
"path": "Chapter 03/myProject/Sources/myProject/Modules/Blog/BlogRouter.swift",
"chars": 288,
"preview": "import Vapor\n\nstruct BlogRouter: RouteCollection {\n \n let controller = BlogFrontendController()\n \n func boot"
},
{
"path": "Chapter 03/myProject/Sources/myProject/Modules/Blog/Controllers/BlogFrontendController.swift",
"chars": 1425,
"preview": "import Vapor\n\nstruct BlogFrontendController {\n \n var posts: [BlogPost] = {\n stride(from: 1, to: 9, by: 1).m"
},
{
"path": "Chapter 03/myProject/Sources/myProject/Modules/Blog/Templates/Contexts/BlogPostContext.swift",
"chars": 50,
"preview": "struct BlogPostContext {\n let post: BlogPost\n}\n"
},
{
"path": "Chapter 03/myProject/Sources/myProject/Modules/Blog/Templates/Contexts/BlogPostsContext.swift",
"chars": 121,
"preview": "struct BlogPostsContext {\n let icon: String\n let title: String\n let message: String\n let posts: [BlogPost]\n}"
},
{
"path": "Chapter 03/myProject/Sources/myProject/Modules/Blog/Templates/Html/BlogPostTemplate.swift",
"chars": 1128,
"preview": "import Vapor\nimport SwiftHtml\n\nstruct BlogPostTemplate: TemplateRepresentable {\n\n var context: BlogPostContext\n \n "
},
{
"path": "Chapter 03/myProject/Sources/myProject/Modules/Blog/Templates/Html/BlogPostsTemplate.swift",
"chars": 1134,
"preview": "import Vapor\nimport SwiftHtml\n\nstruct BlogPostsTemplate: TemplateRepresentable {\n \n var context: BlogPostsContext\n"
},
{
"path": "Chapter 03/myProject/Sources/myProject/Modules/Web/Controllers/WebFrontendController.swift",
"chars": 812,
"preview": "import Vapor\n\nstruct WebFrontendController {\n \n func homeView(req: Request) throws -> Response {\n let ctx ="
},
{
"path": "Chapter 03/myProject/Sources/myProject/Modules/Web/Templates/Contexts/WebHomeContext.swift",
"chars": 151,
"preview": "struct WebHomeContext {\n let icon: String\n let title: String\n let message: String\n let paragraphs: [String]\n"
},
{
"path": "Chapter 03/myProject/Sources/myProject/Modules/Web/Templates/Contexts/WebIndexContext.swift",
"chars": 153,
"preview": "public struct WebIndexContext {\n \n public let title: String\n \n public init(\n title: String\n ) {\n "
},
{
"path": "Chapter 03/myProject/Sources/myProject/Modules/Web/Templates/Contexts/WebLinkContext.swift",
"chars": 223,
"preview": "public struct WebLinkContext {\n \n public let label: String\n public let url: String\n \n public init(\n "
},
{
"path": "Chapter 03/myProject/Sources/myProject/Modules/Web/Templates/Html/WebHomeTemplate.swift",
"chars": 836,
"preview": "import Vapor\nimport SwiftHtml\n\nstruct WebHomeTemplate: TemplateRepresentable {\n\n var context: WebHomeContext\n \n "
},
{
"path": "Chapter 03/myProject/Sources/myProject/Modules/Web/Templates/Html/WebIndexTemplate.swift",
"chars": 3886,
"preview": "import Vapor\nimport SwiftHtml\nimport SwiftSvg\n\nextension Svg {\n \n static func menuIcon() -> Svg {\n Svg {\n "
},
{
"path": "Chapter 03/myProject/Sources/myProject/Modules/Web/Templates/Html/WebLinkTemplate.swift",
"chars": 342,
"preview": "import Vapor\nimport SwiftHtml\n\nstruct WebLinkTemplate: TemplateRepresentable {\n\n var context: WebLinkContext\n \n "
},
{
"path": "Chapter 03/myProject/Sources/myProject/Modules/Web/WebRouter.swift",
"chars": 234,
"preview": "import Vapor\n\nstruct WebRouter: RouteCollection {\n \n let frontendController = WebFrontendController()\n\n func bo"
},
{
"path": "Chapter 03/myProject/Sources/myProject/Template/Request+Template.swift",
"chars": 95,
"preview": "import Vapor\n\npublic extension Request {\n var templates: TemplateRenderer { .init(self) }\n}\n"
},
{
"path": "Chapter 03/myProject/Sources/myProject/Template/TemplateRenderer.swift",
"chars": 691,
"preview": "import Vapor\nimport SwiftHtml\n\npublic struct TemplateRenderer {\n \n var req: Request\n \n init(_ req: Request) "
},
{
"path": "Chapter 03/myProject/Sources/myProject/Template/TemplateRepresentable.swift",
"chars": 133,
"preview": "import Vapor\nimport SwiftSgml\n\npublic protocol TemplateRepresentable {\n \n @TagBuilder\n func render(_ req: Reque"
},
{
"path": "Chapter 03/myProject/Sources/myProject/configure.swift",
"chars": 422,
"preview": "import Vapor\n\npublic func configure(\n _ app: Application\n) async throws {\n\n app.middleware.use(\n FileMiddle"
},
{
"path": "Chapter 03/myProject/Sources/myProject/entrypoint.swift",
"chars": 522,
"preview": "import Vapor\nimport Logging\n\n@main\nenum Entrypoint {\n\n static func main() async throws {\n var env = try Enviro"
},
{
"path": "Chapter 03/myProject/Sources/myProject/routes.swift",
"chars": 361,
"preview": "import Vapor\nimport SwiftHtml\n\nfunc routes(_ app: Application) throws {\n\n app.routes.get { req -> Response in\n "
},
{
"path": "Chapter 03/myProject/Tests/myProjectTests/myProjectTests.swift",
"chars": 460,
"preview": "@testable import myProject\nimport VaporTesting\nimport Testing\n\n@Suite(\"App Tests\")\nstruct myProjectTests {\n\n @Test(\"T"
},
{
"path": "Chapter 03/myProject/docker-compose.yml",
"chars": 854,
"preview": "# Docker Compose file for Vapor\n#\n# Install Docker on your system to run and test\n# your Vapor app in a production-like "
},
{
"path": "Chapter 04/myProject/Dockerfile",
"chars": 3336,
"preview": "# ================================\n# Build image\n# ================================\nFROM swift:6.0-noble AS build\n\n# Ins"
},
{
"path": "Chapter 04/myProject/Package.swift",
"chars": 1245,
"preview": "// swift-tools-version:5.7\nimport PackageDescription\n\nlet package = Package(\n name: \"myProject\",\n platforms: [\n "
},
{
"path": "Chapter 04/myProject/Public/css/web.css",
"chars": 35,
"preview": "#blog h2 {\n margin: 0.5rem 0;\n}\n"
},
{
"path": "Chapter 04/myProject/Public/js/web.js",
"chars": 59,
"preview": "function about() {\n alert(\"myPage\\n\\nversion 1.0.0\");\n}\n"
},
{
"path": "Chapter 04/myProject/Sources/App/Framework/DatabaseModelInterface.swift",
"chars": 520,
"preview": "import Vapor\nimport Fluent\n\npublic protocol DatabaseModelInterface: Fluent.Model\n where Self.IDValue == UUID\n{ \n "
},
{
"path": "Chapter 04/myProject/Sources/App/Framework/ModuleInterface.swift",
"chars": 323,
"preview": "import Vapor\n\npublic protocol ModuleInterface {\n \n static var identifier: String { get }\n\n func boot(_ app: App"
},
{
"path": "Chapter 04/myProject/Sources/App/Middlewares/ExtendPathMiddleware.swift",
"chars": 466,
"preview": "import Vapor\n\nstruct ExtendPathMiddleware: AsyncMiddleware {\n\n func respond(\n to req: Request,\n chainin"
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Blog/BlogModule.swift",
"chars": 285,
"preview": "import Vapor\n\nstruct BlogModule: ModuleInterface {\n\n let router = BlogRouter()\n\n func boot(_ app: Application) thr"
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Blog/BlogRouter.swift",
"chars": 284,
"preview": "import Vapor\n\nstruct BlogRouter: RouteCollection {\n \n let controller = BlogFrontendController()\n \n func boot"
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift",
"chars": 1836,
"preview": "import Vapor\nimport Fluent\n\nstruct BlogFrontendController {\n \n func blogView(req: Request) async throws -> Respons"
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Blog/Database/Migrations/BlogMigrations.swift",
"chars": 2682,
"preview": "import Foundation\nimport Fluent\n\nenum BlogMigrations {\n \n struct v1: AsyncMigration {\n \n func prepar"
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Blog/Database/Models/BlogCategoryModel.swift",
"chars": 581,
"preview": "import Vapor\nimport Fluent\n\nfinal class BlogCategoryModel: DatabaseModelInterface {\n typealias Module = BlogModule\n\n "
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Blog/Database/Models/BlogPostModel.swift",
"chars": 1515,
"preview": "import Vapor\nimport Fluent\n\nfinal class BlogPostModel: DatabaseModelInterface {\n typealias Module = BlogModule\n \n "
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Blog/Objects/Blog.swift",
"chars": 86,
"preview": "enum Blog {\n \n enum Post {\n \n }\n\n enum Category {\n \n }\n}\n"
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Blog/Objects/BlogCategory.swift",
"chars": 132,
"preview": "import Foundation\n\nextension Blog.Category {\n \n struct List: Codable {\n let id: UUID\n let title: Str"
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Blog/Objects/BlogPost.swift",
"chars": 488,
"preview": "import Foundation\n\nextension Blog.Post {\n \n struct List: Codable {\n let id: UUID\n let title: String\n"
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostContext.swift",
"chars": 58,
"preview": "struct BlogPostContext {\n let post: Blog.Post.Detail\n}\n"
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostsContext.swift",
"chars": 127,
"preview": "struct BlogPostsContext {\n let icon: String\n let title: String\n let message: String\n let posts: [Blog.Post.L"
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostTemplate.swift",
"chars": 1128,
"preview": "import Vapor\nimport SwiftHtml\n\nstruct BlogPostTemplate: TemplateRepresentable {\n\n var context: BlogPostContext\n \n "
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostsTemplate.swift",
"chars": 1134,
"preview": "import Vapor\nimport SwiftHtml\n\nstruct BlogPostsTemplate: TemplateRepresentable {\n \n var context: BlogPostsContext\n"
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Web/Controllers/WebFrontendController.swift",
"chars": 812,
"preview": "import Vapor\n\nstruct WebFrontendController {\n \n func homeView(req: Request) throws -> Response {\n let ctx ="
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Web/Templates/Contexts/WebHomeContext.swift",
"chars": 151,
"preview": "struct WebHomeContext {\n let icon: String\n let title: String\n let message: String\n let paragraphs: [String]\n"
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Web/Templates/Contexts/WebIndexContext.swift",
"chars": 153,
"preview": "public struct WebIndexContext {\n \n public let title: String\n \n public init(\n title: String\n ) {\n "
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Web/Templates/Contexts/WebLinkContext.swift",
"chars": 223,
"preview": "public struct WebLinkContext {\n \n public let label: String\n public let url: String\n \n public init(\n "
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Web/Templates/Html/WebHomeTemplate.swift",
"chars": 836,
"preview": "import Vapor\nimport SwiftHtml\n\nstruct WebHomeTemplate: TemplateRepresentable {\n\n var context: WebHomeContext\n \n "
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift",
"chars": 3947,
"preview": "import Vapor\nimport SwiftHtml\nimport SwiftSvg\n\nextension Svg {\n \n static func menuIcon() -> Svg {\n Svg {\n "
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Web/Templates/Html/WebLinkTemplate.swift",
"chars": 342,
"preview": "import Vapor\nimport SwiftHtml\n\nstruct WebLinkTemplate: TemplateRepresentable {\n\n var context: WebLinkContext\n \n "
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Web/WebModule.swift",
"chars": 176,
"preview": "import Vapor\n\nstruct WebModule: ModuleInterface {\n\n let router = WebRouter()\n\n func boot(_ app: Application) throw"
},
{
"path": "Chapter 04/myProject/Sources/App/Modules/Web/WebRouter.swift",
"chars": 230,
"preview": "import Vapor\n\nstruct WebRouter: RouteCollection {\n \n let frontendController = WebFrontendController()\n\n func bo"
},
{
"path": "Chapter 04/myProject/Sources/App/Template/Request+Template.swift",
"chars": 95,
"preview": "import Vapor\n\npublic extension Request {\n var templates: TemplateRenderer { .init(self) }\n}\n"
},
{
"path": "Chapter 04/myProject/Sources/App/Template/TemplateRenderer.swift",
"chars": 687,
"preview": "import Vapor\nimport SwiftHtml\n\npublic struct TemplateRenderer {\n \n var req: Request\n \n init(_ req: Request) "
},
{
"path": "Chapter 04/myProject/Sources/App/Template/TemplateRepresentable.swift",
"chars": 133,
"preview": "import Vapor\nimport SwiftSgml\n\npublic protocol TemplateRepresentable {\n \n @TagBuilder\n func render(_ req: Reque"
},
{
"path": "Chapter 04/myProject/Sources/App/configure.swift",
"chars": 603,
"preview": "import Vapor\nimport Fluent\nimport FluentSQLiteDriver\n\npublic func configure(\n _ app: Application\n) throws {\n\n let "
},
{
"path": "Chapter 04/myProject/Sources/App/routes.swift",
"chars": 361,
"preview": "import Vapor\nimport SwiftHtml\n\nfunc routes(_ app: Application) throws {\n\n app.routes.get { req -> Response in\n "
},
{
"path": "Chapter 04/myProject/Sources/Run/main.swift",
"chars": 185,
"preview": "import App\nimport Vapor\n\nvar env = try Environment.detect()\ntry LoggingSystem.bootstrap(from: &env)\nlet app = Applicatio"
},
{
"path": "Chapter 04/myProject/Tests/AppTests/AppTests.swift",
"chars": 393,
"preview": "@testable import App\nimport XCTVapor\n\nfinal class AppTests: XCTestCase {\n func testHelloWorld() throws {\n let "
},
{
"path": "Chapter 04/myProject/docker-compose.yml",
"chars": 854,
"preview": "# Docker Compose file for Vapor\n#\n# Install Docker on your system to run and test\n# your Vapor app in a production-like "
},
{
"path": "Chapter 05/myProject/Dockerfile",
"chars": 3336,
"preview": "# ================================\n# Build image\n# ================================\nFROM swift:6.0-noble AS build\n\n# Ins"
},
{
"path": "Chapter 05/myProject/Package.swift",
"chars": 1245,
"preview": "// swift-tools-version:5.7\nimport PackageDescription\n\nlet package = Package(\n name: \"myProject\",\n platforms: [\n "
},
{
"path": "Chapter 05/myProject/Public/css/web.css",
"chars": 35,
"preview": "#blog h2 {\n margin: 0.5rem 0;\n}\n"
},
{
"path": "Chapter 05/myProject/Public/js/web.js",
"chars": 59,
"preview": "function about() {\n alert(\"myPage\\n\\nversion 1.0.0\");\n}\n"
},
{
"path": "Chapter 05/myProject/Sources/App/Framework/AuthenticatedUser.swift",
"chars": 323,
"preview": "import Vapor\n\npublic struct AuthenticatedUser {\n\n public let id: UUID\n public let email: String\n \n public in"
},
{
"path": "Chapter 05/myProject/Sources/App/Framework/DatabaseModelInterface.swift",
"chars": 520,
"preview": "import Vapor\nimport Fluent\n\npublic protocol DatabaseModelInterface: Fluent.Model\n where Self.IDValue == UUID\n{ \n "
},
{
"path": "Chapter 05/myProject/Sources/App/Framework/ModuleInterface.swift",
"chars": 323,
"preview": "import Vapor\n\npublic protocol ModuleInterface {\n \n static var identifier: String { get }\n\n func boot(_ app: App"
},
{
"path": "Chapter 05/myProject/Sources/App/Middlewares/ExtendPathMiddleware.swift",
"chars": 466,
"preview": "import Vapor\n\nstruct ExtendPathMiddleware: AsyncMiddleware {\n\n func respond(\n to req: Request,\n chainin"
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Blog/BlogModule.swift",
"chars": 285,
"preview": "import Vapor\n\nstruct BlogModule: ModuleInterface {\n\n let router = BlogRouter()\n\n func boot(_ app: Application) thr"
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Blog/BlogRouter.swift",
"chars": 284,
"preview": "import Vapor\n\nstruct BlogRouter: RouteCollection {\n \n let controller = BlogFrontendController()\n \n func boot"
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift",
"chars": 1836,
"preview": "import Vapor\nimport Fluent\n\nstruct BlogFrontendController {\n \n func blogView(req: Request) async throws -> Respons"
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Blog/Database/Migrations/BlogMigrations.swift",
"chars": 2682,
"preview": "import Foundation\nimport Fluent\n\nenum BlogMigrations {\n \n struct v1: AsyncMigration {\n \n func prepar"
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Blog/Database/Models/BlogCategoryModel.swift",
"chars": 581,
"preview": "import Vapor\nimport Fluent\n\nfinal class BlogCategoryModel: DatabaseModelInterface {\n typealias Module = BlogModule\n\n "
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Blog/Database/Models/BlogPostModel.swift",
"chars": 1515,
"preview": "import Vapor\nimport Fluent\n\nfinal class BlogPostModel: DatabaseModelInterface {\n typealias Module = BlogModule\n \n "
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Blog/Objects/Blog.swift",
"chars": 86,
"preview": "enum Blog {\n \n enum Post {\n \n }\n\n enum Category {\n \n }\n}\n"
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Blog/Objects/BlogCategory.swift",
"chars": 132,
"preview": "import Foundation\n\nextension Blog.Category {\n \n struct List: Codable {\n let id: UUID\n let title: Str"
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Blog/Objects/BlogPost.swift",
"chars": 488,
"preview": "import Foundation\n\nextension Blog.Post {\n \n struct List: Codable {\n let id: UUID\n let title: String\n"
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostContext.swift",
"chars": 58,
"preview": "struct BlogPostContext {\n let post: Blog.Post.Detail\n}\n"
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostsContext.swift",
"chars": 127,
"preview": "struct BlogPostsContext {\n let icon: String\n let title: String\n let message: String\n let posts: [Blog.Post.L"
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostTemplate.swift",
"chars": 1128,
"preview": "import Vapor\nimport SwiftHtml\n\nstruct BlogPostTemplate: TemplateRepresentable {\n\n var context: BlogPostContext\n \n "
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostsTemplate.swift",
"chars": 1134,
"preview": "import Vapor\nimport SwiftHtml\n\nstruct BlogPostsTemplate: TemplateRepresentable {\n \n var context: BlogPostsContext\n"
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/User/Authenticators/UserCredentialsAuthenticator.swift",
"chars": 952,
"preview": "import Vapor\nimport Fluent\n\nstruct UserCredentialsAuthenticator: AsyncCredentialsAuthenticator {\n \n struct Credent"
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/User/Authenticators/UserSessionAuthenticator.swift",
"chars": 541,
"preview": "import Vapor\nimport Fluent\n\nstruct UserSessionAuthenticator: AsyncSessionAuthenticator {\n typealias User = Authentica"
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/User/Controllers/UserFrontendController.swift",
"chars": 1626,
"preview": "import Vapor\n\nstruct UserFrontendController {\n \n private struct Input: Decodable {\n let email: String?\n "
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/User/Database/Migrations/UserMigrations.swift",
"chars": 1135,
"preview": "import Vapor\nimport Fluent\n\nenum UserMigrations {\n \n struct v1: AsyncMigration {\n \n func prepare(on "
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/User/Database/Models/UserAccountModel.swift",
"chars": 659,
"preview": "import Vapor\nimport Fluent\n\nfinal class UserAccountModel: DatabaseModelInterface {\n typealias Module = UserModule\n "
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/User/Templates/Contexts/UserLoginContext.swift",
"chars": 533,
"preview": "struct UserLoginContext {\n \n let icon: String\n let title: String\n let message: String\n let email: String?"
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/User/Templates/Html/UserLoginTemplate.swift",
"chars": 2015,
"preview": "import Vapor\nimport SwiftHtml\n\nstruct UserLoginTemplate: TemplateRepresentable {\n \n var context: UserLoginContext\n"
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/User/UserModule.swift",
"chars": 349,
"preview": "import Vapor\n\nstruct UserModule: ModuleInterface {\n\n let router = UserRouter()\n\n func boot(_ app: Application) thr"
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/User/UserRouter.swift",
"chars": 496,
"preview": "import Vapor\n\nstruct UserRouter: RouteCollection {\n \n let frontendController = UserFrontendController()\n \n f"
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Web/Controllers/WebFrontendController.swift",
"chars": 812,
"preview": "import Vapor\n\nstruct WebFrontendController {\n \n func homeView(req: Request) throws -> Response {\n let ctx ="
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Web/Templates/Contexts/WebHomeContext.swift",
"chars": 151,
"preview": "struct WebHomeContext {\n let icon: String\n let title: String\n let message: String\n let paragraphs: [String]\n"
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Web/Templates/Contexts/WebIndexContext.swift",
"chars": 153,
"preview": "public struct WebIndexContext {\n \n public let title: String\n \n public init(\n title: String\n ) {\n "
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Web/Templates/Contexts/WebLinkContext.swift",
"chars": 223,
"preview": "public struct WebLinkContext {\n \n public let label: String\n public let url: String\n \n public init(\n "
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Web/Templates/Html/WebHomeTemplate.swift",
"chars": 836,
"preview": "import Vapor\nimport SwiftHtml\n\nstruct WebHomeTemplate: TemplateRepresentable {\n\n var context: WebHomeContext\n \n "
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift",
"chars": 4379,
"preview": "import Vapor\nimport SwiftHtml\nimport SwiftSvg\n\nextension Svg {\n \n static func menuIcon() -> Svg {\n Svg {\n "
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Web/Templates/Html/WebLinkTemplate.swift",
"chars": 342,
"preview": "import Vapor\nimport SwiftHtml\n\nstruct WebLinkTemplate: TemplateRepresentable {\n\n var context: WebLinkContext\n \n "
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Web/WebModule.swift",
"chars": 176,
"preview": "import Vapor\n\nstruct WebModule: ModuleInterface {\n\n let router = WebRouter()\n\n func boot(_ app: Application) throw"
},
{
"path": "Chapter 05/myProject/Sources/App/Modules/Web/WebRouter.swift",
"chars": 230,
"preview": "import Vapor\n\nstruct WebRouter: RouteCollection {\n \n let frontendController = WebFrontendController()\n\n func bo"
},
{
"path": "Chapter 05/myProject/Sources/App/Template/Request+Template.swift",
"chars": 95,
"preview": "import Vapor\n\npublic extension Request {\n var templates: TemplateRenderer { .init(self) }\n}\n"
},
{
"path": "Chapter 05/myProject/Sources/App/Template/TemplateRenderer.swift",
"chars": 687,
"preview": "import Vapor\nimport SwiftHtml\n\npublic struct TemplateRenderer {\n \n var req: Request\n \n init(_ req: Request) "
},
{
"path": "Chapter 05/myProject/Sources/App/Template/TemplateRepresentable.swift",
"chars": 133,
"preview": "import Vapor\nimport SwiftSgml\n\npublic protocol TemplateRepresentable {\n \n @TagBuilder\n func render(_ req: Reque"
},
{
"path": "Chapter 05/myProject/Sources/App/configure.swift",
"chars": 752,
"preview": "import Vapor\nimport Fluent\nimport FluentSQLiteDriver\n\npublic func configure(\n _ app: Application\n) throws {\n\n let "
},
{
"path": "Chapter 05/myProject/Sources/App/routes.swift",
"chars": 361,
"preview": "import Vapor\nimport SwiftHtml\n\nfunc routes(_ app: Application) throws {\n\n app.routes.get { req -> Response in\n "
},
{
"path": "Chapter 05/myProject/Sources/Run/main.swift",
"chars": 185,
"preview": "import App\nimport Vapor\n\nvar env = try Environment.detect()\ntry LoggingSystem.bootstrap(from: &env)\nlet app = Applicatio"
},
{
"path": "Chapter 05/myProject/Tests/AppTests/AppTests.swift",
"chars": 393,
"preview": "@testable import App\nimport XCTVapor\n\nfinal class AppTests: XCTestCase {\n func testHelloWorld() throws {\n let "
},
{
"path": "Chapter 05/myProject/docker-compose.yml",
"chars": 854,
"preview": "# Docker Compose file for Vapor\n#\n# Install Docker on your system to run and test\n# your Vapor app in a production-like "
},
{
"path": "Chapter 06/myProject/Dockerfile",
"chars": 3336,
"preview": "# ================================\n# Build image\n# ================================\nFROM swift:6.0-noble AS build\n\n# Ins"
},
{
"path": "Chapter 06/myProject/Package.swift",
"chars": 1245,
"preview": "// swift-tools-version:5.7\nimport PackageDescription\n\nlet package = Package(\n name: \"myProject\",\n platforms: [\n "
},
{
"path": "Chapter 06/myProject/Public/css/web.css",
"chars": 35,
"preview": "#blog h2 {\n margin: 0.5rem 0;\n}\n"
},
{
"path": "Chapter 06/myProject/Public/js/web.js",
"chars": 59,
"preview": "function about() {\n alert(\"myPage\\n\\nversion 1.0.0\");\n}\n"
},
{
"path": "Chapter 06/myProject/Sources/App/Framework/AuthenticatedUser.swift",
"chars": 323,
"preview": "import Vapor\n\npublic struct AuthenticatedUser {\n\n public let id: UUID\n public let email: String\n \n public in"
},
{
"path": "Chapter 06/myProject/Sources/App/Framework/DatabaseModelInterface.swift",
"chars": 520,
"preview": "import Vapor\nimport Fluent\n\npublic protocol DatabaseModelInterface: Fluent.Model\n where Self.IDValue == UUID\n{ \n "
},
{
"path": "Chapter 06/myProject/Sources/App/Framework/Form/AbstractForm.swift",
"chars": 1794,
"preview": "import Vapor\n\nopen class AbstractForm: FormComponent {\n \n open var action: FormAction\n open var fields: [FormCo"
},
{
"path": "Chapter 06/myProject/Sources/App/Framework/Form/AbstractFormField.swift",
"chars": 1272,
"preview": "import Vapor\n\nopen class AbstractFormField<\n Input: Decodable,\n Output: TemplateRepresentable\n>: FormComponent {\n "
},
{
"path": "Chapter 06/myProject/Sources/App/Framework/Form/Fields/InputField.swift",
"chars": 328,
"preview": "public final class InputField: AbstractFormField<\n String,\n InputFieldTemplate\n> {\n\n public convenience init(_ "
},
{
"path": "Chapter 06/myProject/Sources/App/Framework/Form/FormAction.swift",
"chars": 394,
"preview": "import SwiftHtml\n\npublic struct FormAction {\n \n public var method: SwiftHtml.Method\n public var url: String?\n "
},
{
"path": "Chapter 06/myProject/Sources/App/Framework/Form/FormComponent.swift",
"chars": 370,
"preview": "import Vapor\n\npublic protocol FormComponent {\n \n func load(req: Request) async throws\n func process(req: Reques"
},
{
"path": "Chapter 06/myProject/Sources/App/Framework/Form/FormComponentBuilder.swift",
"chars": 183,
"preview": "@resultBuilder\npublic enum FormComponentBuilder {\n \n public static func buildBlock(\n _ components: FormComp"
},
{
"path": "Chapter 06/myProject/Sources/App/Framework/Form/Templates/Contexts/FormContext.swift",
"chars": 172,
"preview": "public struct FormContext {\n public var action: FormAction\n public var fields: [TemplateRepresentable]\n public "
},
{
"path": "Chapter 06/myProject/Sources/App/Framework/Form/Templates/Contexts/InputFieldContext.swift",
"chars": 672,
"preview": "import SwiftHtml\n\npublic struct InputFieldContext {\n \n public let key: String\n public var label: LabelContext\n "
},
{
"path": "Chapter 06/myProject/Sources/App/Framework/Form/Templates/Contexts/LabelContext.swift",
"chars": 407,
"preview": "public struct LabelContext {\n \n public var key: String\n public var title: String?\n public var required: Bool"
},
{
"path": "Chapter 06/myProject/Sources/App/Framework/Form/Templates/Html/FormTemplate.swift",
"chars": 912,
"preview": "import Vapor\nimport SwiftHtml\n\npublic struct FormTemplate: TemplateRepresentable {\n \n var context: FormContext\n "
},
{
"path": "Chapter 06/myProject/Sources/App/Framework/Form/Templates/Html/InputFieldTemplate.swift",
"chars": 668,
"preview": "import Vapor\nimport SwiftHtml\n\npublic struct InputFieldTemplate: TemplateRepresentable {\n\n public var context: InputF"
},
{
"path": "Chapter 06/myProject/Sources/App/Framework/Form/Templates/Html/LabelTemplate.swift",
"chars": 630,
"preview": "import Vapor\nimport SwiftHtml\n\npublic struct LabelTemplate: TemplateRepresentable {\n\n var context: LabelContext\n\n "
},
{
"path": "Chapter 06/myProject/Sources/App/Framework/ModuleInterface.swift",
"chars": 323,
"preview": "import Vapor\n\npublic protocol ModuleInterface {\n \n static var identifier: String { get }\n\n func boot(_ app: App"
},
{
"path": "Chapter 06/myProject/Sources/App/Middlewares/ExtendPathMiddleware.swift",
"chars": 466,
"preview": "import Vapor\n\nstruct ExtendPathMiddleware: AsyncMiddleware {\n\n func respond(\n to req: Request,\n chainin"
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Blog/BlogModule.swift",
"chars": 285,
"preview": "import Vapor\n\nstruct BlogModule: ModuleInterface {\n\n let router = BlogRouter()\n\n func boot(_ app: Application) thr"
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Blog/BlogRouter.swift",
"chars": 284,
"preview": "import Vapor\n\nstruct BlogRouter: RouteCollection {\n \n let controller = BlogFrontendController()\n \n func boot"
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Blog/Controllers/BlogFrontendController.swift",
"chars": 1836,
"preview": "import Vapor\nimport Fluent\n\nstruct BlogFrontendController {\n \n func blogView(req: Request) async throws -> Respons"
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Blog/Database/Migrations/BlogMigrations.swift",
"chars": 2682,
"preview": "import Foundation\nimport Fluent\n\nenum BlogMigrations {\n \n struct v1: AsyncMigration {\n \n func prepar"
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Blog/Database/Models/BlogCategoryModel.swift",
"chars": 581,
"preview": "import Vapor\nimport Fluent\n\nfinal class BlogCategoryModel: DatabaseModelInterface {\n typealias Module = BlogModule\n\n "
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Blog/Database/Models/BlogPostModel.swift",
"chars": 1515,
"preview": "import Vapor\nimport Fluent\n\nfinal class BlogPostModel: DatabaseModelInterface {\n typealias Module = BlogModule\n \n "
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Blog/Objects/Blog.swift",
"chars": 86,
"preview": "enum Blog {\n \n enum Post {\n \n }\n\n enum Category {\n \n }\n}\n"
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Blog/Objects/BlogCategory.swift",
"chars": 132,
"preview": "import Foundation\n\nextension Blog.Category {\n \n struct List: Codable {\n let id: UUID\n let title: Str"
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Blog/Objects/BlogPost.swift",
"chars": 488,
"preview": "import Foundation\n\nextension Blog.Post {\n \n struct List: Codable {\n let id: UUID\n let title: String\n"
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostContext.swift",
"chars": 58,
"preview": "struct BlogPostContext {\n let post: Blog.Post.Detail\n}\n"
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Blog/Templates/Contexts/BlogPostsContext.swift",
"chars": 127,
"preview": "struct BlogPostsContext {\n let icon: String\n let title: String\n let message: String\n let posts: [Blog.Post.L"
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostTemplate.swift",
"chars": 1128,
"preview": "import Vapor\nimport SwiftHtml\n\nstruct BlogPostTemplate: TemplateRepresentable {\n\n var context: BlogPostContext\n \n "
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Blog/Templates/Html/BlogPostsTemplate.swift",
"chars": 1134,
"preview": "import Vapor\nimport SwiftHtml\n\nstruct BlogPostsTemplate: TemplateRepresentable {\n \n var context: BlogPostsContext\n"
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/User/Authenticators/UserCredentialsAuthenticator.swift",
"chars": 952,
"preview": "import Vapor\nimport Fluent\n\nstruct UserCredentialsAuthenticator: AsyncCredentialsAuthenticator {\n \n struct Credent"
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/User/Authenticators/UserSessionAuthenticator.swift",
"chars": 541,
"preview": "import Vapor\nimport Fluent\n\nstruct UserSessionAuthenticator: AsyncSessionAuthenticator {\n typealias User = Authentica"
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/User/Controllers/UserFrontendController.swift",
"chars": 1327,
"preview": "import Vapor\n\nstruct UserFrontendController {\n \n private func renderSignInView(\n _ req: Request,\n _ "
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/User/Database/Migrations/UserMigrations.swift",
"chars": 1135,
"preview": "import Vapor\nimport Fluent\n\nenum UserMigrations {\n \n struct v1: AsyncMigration {\n \n func prepare(on "
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/User/Database/Models/UserAccountModel.swift",
"chars": 659,
"preview": "import Vapor\nimport Fluent\n\nfinal class UserAccountModel: DatabaseModelInterface {\n typealias Module = UserModule\n "
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/User/Forms/UserLoginForm.swift",
"chars": 723,
"preview": "import Vapor\n\nfinal class UserLoginForm: AbstractForm {\n \n public convenience init() {\n self.init(\n "
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/User/Templates/Contexts/UserLoginContext.swift",
"chars": 379,
"preview": "struct UserLoginContext {\n \n let icon: String\n let title: String\n let message: String\n let form: Template"
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/User/Templates/Html/UserLoginTemplate.swift",
"chars": 725,
"preview": "import Vapor\nimport SwiftHtml\n\nstruct UserLoginTemplate: TemplateRepresentable {\n\n var context: UserLoginContext\n "
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/User/UserModule.swift",
"chars": 349,
"preview": "import Vapor\n\nstruct UserModule: ModuleInterface {\n\n let router = UserRouter()\n\n func boot(_ app: Application) thr"
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/User/UserRouter.swift",
"chars": 496,
"preview": "import Vapor\n\nstruct UserRouter: RouteCollection {\n \n let frontendController = UserFrontendController()\n \n f"
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Web/Controllers/WebFrontendController.swift",
"chars": 812,
"preview": "import Vapor\n\nstruct WebFrontendController {\n \n func homeView(req: Request) throws -> Response {\n let ctx ="
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Web/Templates/Contexts/WebHomeContext.swift",
"chars": 151,
"preview": "struct WebHomeContext {\n let icon: String\n let title: String\n let message: String\n let paragraphs: [String]\n"
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Web/Templates/Contexts/WebIndexContext.swift",
"chars": 153,
"preview": "public struct WebIndexContext {\n \n public let title: String\n \n public init(\n title: String\n ) {\n "
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Web/Templates/Contexts/WebLinkContext.swift",
"chars": 223,
"preview": "public struct WebLinkContext {\n \n public let label: String\n public let url: String\n \n public init(\n "
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Web/Templates/Html/WebHomeTemplate.swift",
"chars": 836,
"preview": "import Vapor\nimport SwiftHtml\n\nstruct WebHomeTemplate: TemplateRepresentable {\n\n var context: WebHomeContext\n \n "
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Web/Templates/Html/WebIndexTemplate.swift",
"chars": 4379,
"preview": "import Vapor\nimport SwiftHtml\nimport SwiftSvg\n\nextension Svg {\n \n static func menuIcon() -> Svg {\n Svg {\n "
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Web/Templates/Html/WebLinkTemplate.swift",
"chars": 342,
"preview": "import Vapor\nimport SwiftHtml\n\nstruct WebLinkTemplate: TemplateRepresentable {\n\n var context: WebLinkContext\n \n "
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Web/WebModule.swift",
"chars": 176,
"preview": "import Vapor\n\nstruct WebModule: ModuleInterface {\n\n let router = WebRouter()\n\n func boot(_ app: Application) throw"
},
{
"path": "Chapter 06/myProject/Sources/App/Modules/Web/WebRouter.swift",
"chars": 230,
"preview": "import Vapor\n\nstruct WebRouter: RouteCollection {\n \n let frontendController = WebFrontendController()\n\n func bo"
},
{
"path": "Chapter 06/myProject/Sources/App/Template/Request+Template.swift",
"chars": 95,
"preview": "import Vapor\n\npublic extension Request {\n var templates: TemplateRenderer { .init(self) }\n}\n"
},
{
"path": "Chapter 06/myProject/Sources/App/Template/TemplateRenderer.swift",
"chars": 687,
"preview": "import Vapor\nimport SwiftHtml\n\npublic struct TemplateRenderer {\n \n var req: Request\n \n init(_ req: Request) "
},
{
"path": "Chapter 06/myProject/Sources/App/Template/TemplateRepresentable.swift",
"chars": 133,
"preview": "import Vapor\nimport SwiftSgml\n\npublic protocol TemplateRepresentable {\n \n @TagBuilder\n func render(_ req: Reque"
},
{
"path": "Chapter 06/myProject/Sources/App/configure.swift",
"chars": 752,
"preview": "import Vapor\nimport Fluent\nimport FluentSQLiteDriver\n\npublic func configure(\n _ app: Application\n) throws {\n\n let "
},
{
"path": "Chapter 06/myProject/Sources/App/routes.swift",
"chars": 361,
"preview": "import Vapor\nimport SwiftHtml\n\nfunc routes(_ app: Application) throws {\n\n app.routes.get { req -> Response in\n "
},
{
"path": "Chapter 06/myProject/Sources/Run/main.swift",
"chars": 185,
"preview": "import App\nimport Vapor\n\nvar env = try Environment.detect()\ntry LoggingSystem.bootstrap(from: &env)\nlet app = Applicatio"
},
{
"path": "Chapter 06/myProject/Tests/AppTests/AppTests.swift",
"chars": 393,
"preview": "@testable import App\nimport XCTVapor\n\nfinal class AppTests: XCTestCase {\n func testHelloWorld() throws {\n let "
},
{
"path": "Chapter 06/myProject/docker-compose.yml",
"chars": 854,
"preview": "# Docker Compose file for Vapor\n#\n# Install Docker on your system to run and test\n# your Vapor app in a production-like "
},
{
"path": "Chapter 07/myProject/Dockerfile",
"chars": 3336,
"preview": "# ================================\n# Build image\n# ================================\nFROM swift:6.0-noble AS build\n\n# Ins"
},
{
"path": "Chapter 07/myProject/Package.swift",
"chars": 1245,
"preview": "// swift-tools-version:5.7\nimport PackageDescription\n\nlet package = Package(\n name: \"myProject\",\n platforms: [\n "
},
{
"path": "Chapter 07/myProject/Public/css/web.css",
"chars": 35,
"preview": "#blog h2 {\n margin: 0.5rem 0;\n}\n"
},
{
"path": "Chapter 07/myProject/Public/js/web.js",
"chars": 59,
"preview": "function about() {\n alert(\"myPage\\n\\nversion 1.0.0\");\n}\n"
},
{
"path": "Chapter 07/myProject/Sources/App/Framework/AuthenticatedUser.swift",
"chars": 323,
"preview": "import Vapor\n\npublic struct AuthenticatedUser {\n\n public let id: UUID\n public let email: String\n \n public in"
},
{
"path": "Chapter 07/myProject/Sources/App/Framework/DatabaseModelInterface.swift",
"chars": 520,
"preview": "import Vapor\nimport Fluent\n\npublic protocol DatabaseModelInterface: Fluent.Model\n where Self.IDValue == UUID\n{ \n "
},
{
"path": "Chapter 07/myProject/Sources/App/Framework/Form/AbstractForm.swift",
"chars": 1794,
"preview": "import Vapor\n\nopen class AbstractForm: FormComponent {\n \n open var action: FormAction\n open var fields: [FormCo"
},
{
"path": "Chapter 07/myProject/Sources/App/Framework/Form/AbstractFormField.swift",
"chars": 2776,
"preview": "import Vapor\n\nopen class AbstractFormField<\n Input: Decodable,\n Output: TemplateRepresentable\n>: FormComponent {\n "
},
{
"path": "Chapter 07/myProject/Sources/App/Framework/Form/Fields/InputField.swift",
"chars": 328,
"preview": "public final class InputField: AbstractFormField<\n String,\n InputFieldTemplate\n> {\n\n public convenience init(_ "
}
]
// ... and 1355 more files (download for full content)
About this extraction
This page contains the full source code of the tib/practical-server-side-swift GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 1555 files (1.3 MB), approximately 357.6k tokens, and a symbol index with 14 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.